npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vanilla-select-js

v1.0.0-beta.0

Published

Dependency-free vanilla JS select/dropdown inspired by react-select and Select2 with async search, debounce, nested tree options, and optional React adapter.

Readme

Vanilla Select JS

vanilla-select-js is a dependency-free select/dropdown package for modern web apps.

It is inspired by react-select and Select2, while keeping the runtime fully vanilla JavaScript and framework-agnostic.

Why this package

  • No runtime dependencies in the core package
  • Async search with built-in debounce
  • Tree/nested options with unlimited depth
  • Single and multi-select support
  • Optional React adapter with TypeScript props
  • SCSS + CSS variable based customization
  • Default runtime style injection (no mandatory CSS import)

Installation

npm install vanilla-select-js

Compatibility

| Dependency | Version | | --- | --- | | Node.js | >= 14.x (recommended >= 18.x) | | React | ^16.8.0 || ^17.0.0 || ^18.0.0 | | react-dom | ^16.8.0 || ^17.0.0 || ^18.0.0 |

React/react-dom are only required when using vanilla-select-js/react. The core package stays framework-agnostic.

Overall Use Cases

  • Basic single select for forms
  • Multi-select with optional checkboxes (showCheckbox)
  • Async remote option loading with debounce (loadOptions)
  • Nested/tree select menus (children / options)
  • Controlled menu debugging (menuIsOpen)
  • Flexible preselected values (primitive/object inputs normalized automatically)
  • Integration inside React, Vue, Angular, or plain JavaScript projects

Search Support (React-Select Style)

Yes, searching is supported.

  • searchable (boolean, default true): like isSearchable in react-select.
  • loadOptions (async or sync function): like AsyncSelect-style option loading.
  • debounce (number, default 250): built-in search debounce delay in ms.
  • onSearch (optional callback): fires after search/filter cycle with { query, total, loading }.

Notes:

  • For static options, typing filters existing options.
  • For remote options, provide loadOptions(query) and return option objects.
  • onSearch is notification-only. Search/filter still works even if onSearch is not provided.
  • onSearch runs after debounce, so console logs are not always instant on each keypress.

Demo Screenshots

Single Select

Single Select Demo

Nested Multi Select

Nested Multi Select Demo

Async Search

Async Search Demo

Basic Usage (Vanilla)

<div id="framework-select"></div>
<script type="module">
  import Select from "vanilla-select-js";

  const select = new Select("#framework-select", {
    options: [
      { label: "React", value: "react" },
      { label: "Vue", value: "vue" },
      { label: "Angular", value: "angular" }
    ],
    onChange: async (value, detail) => {
      // single: { label, value } | null
      // multi: [{ label, value }, ...]
      console.log(value, detail);
    }
  });

  window.select = select;
</script>

Named import aliases are also available:

import { Select, JSSelect } from "vanilla-select-js";

Async Search + Debounce

new Select("#users", {
  debounce: 300,
  loadOptions: async (query) => {
    const response = await fetch(`/api/users?search=${encodeURIComponent(query)}`);
    const users = await response.json();
    return users.map((u) => ({ label: u.name, value: u.id }));
  }
});

Searchable (Local Options)

new Select("#languages", {
  searchable: true,
  options: [
    { label: "JavaScript", value: "js" },
    { label: "TypeScript", value: "ts" },
    { label: "Python", value: "py" },
    { label: "Go", value: "go" },
    { label: "Rust", value: "rust" }
  ],
  onSearch: async ({ query, total }) => {
    console.log("query:", query, "matched:", total);
  }
});

Nested/Tree Select

new Select("#tree", {
  multiple: true,
  showCheckbox: true,
  options: [
    {
      label: "Frontend",
      value: "frontend",
      children: [
        {
          label: "Frameworks",
          value: "frameworks",
          children: [
            { label: "React", value: "react" },
            { label: "Vue", value: "vue" },
            { label: "Angular", value: "angular" }
          ]
        }
      ]
    }
  ]
});

Default tree behavior is toggle-only for parent nodes (selectableParents: false). If needed, make a specific parent selectable with selectable: true.

Value Normalization + menuIsOpen

Single select accepts either:

value: "react";
value: { label: "React", value: "react" };

Multi-select accepts either:

value: ["react", "vue"];
value: [{ label: "React", value: "react" }, { label: "Vue", value: "vue" }];

Controlled menu example:

new Select("#debug-select", {
  options: [
    { label: "React", value: "react" },
    { label: "Vue", value: "vue" }
  ],
  menuIsOpen: true
});

React Usage (Typed TSX)

import { Select } from "vanilla-select-js/react";
import type { JSSelectProps, JSSelectOption } from "vanilla-select-js/react";

type FrameworkOption = JSSelectOption<string>;

const options: FrameworkOption[] = [
  { label: "React", value: "react" },
  { label: "Vue", value: "vue" },
  { label: "Angular", value: "angular" }
];

export default function FrameworkSelect() {
  const props: JSSelectProps<string, FrameworkOption> = {
    options,
    multiple: true,
    showCheckbox: true,
    menuIsOpen: false,
    messages: { placeholder: "Pick frameworks" }
  };

  return (
    <Select
      {...props}
      onChange={async (selected) => {
        console.log(selected);
      }}
    />
  );
}

Backward-compatible React alias is still available:

import { JSSelect } from "vanilla-select-js/react";

Hovering over <Select /> or <JSSelect /> in TS/TSX shows the available typed props.

Framework Wrapper Examples

Vue

<script setup>
import { onMounted, onBeforeUnmount, ref } from "vue";
import Select from "vanilla-select-js";

const root = ref(null);
let instance;

onMounted(() => {
  instance = new Select(root.value, {
    options: [{ label: "Vue", value: "vue" }]
  });
});

onBeforeUnmount(() => {
  instance?.destroy();
});
</script>

<template>
  <div ref="root" />
</template>

Angular

import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild } from "@angular/core";
import Select from "vanilla-select-js";

@Component({
  selector: "app-select",
  template: "<div #selectRoot></div>"
})
export class SelectComponent implements AfterViewInit, OnDestroy {
  @ViewChild("selectRoot", { static: true }) selectRoot!: ElementRef<HTMLElement>;
  private instance?: Select;

  ngAfterViewInit(): void {
    this.instance = new Select(this.selectRoot.nativeElement, {
      options: [{ label: "Angular", value: "angular" }]
    });
  }

  ngOnDestroy(): void {
    this.instance?.destroy();
  }
}

Styling and Customization

1) Default styles (auto-injected)

By default, styles are injected automatically at runtime (injectStyles: true). You do not need to import style.css for normal usage.

new Select("#default-styled", {
  options: [{ label: "React", value: "react" }]
});

Disable injection if you want full manual style control:

new Select("#manual-style", {
  injectStyles: false,
  options: [{ label: "React", value: "react" }]
});

2) SCSS token overrides (build-time)

style.scss is still exported for advanced theming.

@use "vanilla-select-js/style.scss" with (
  $js-select-border-color: #1f2937,
  $js-select-control-bg: #f8fafc,
  $js-select-option-selected-bg: #dbeafe
);

3) CSS custom properties (runtime)

:root {
  --js-select-border: #1f2937;
  --js-select-bg: #ffffff;
  --js-select-option-selected: #dbeafe;
}

Class and Suffix Use Cases

Use className when you need a wrapper hook for page-level layout/theming:

new Select("#layout-hook", {
  className: "settings-form-select",
  options: [{ label: "React", value: "react" }]
});

Use classNamePrefix to generate predictable slot class names for design systems:

new Select("#design-system", {
  classNamePrefix: "acme-select",
  options: [{ label: "React", value: "react" }]
});

Use suffix to add a BEM modifier token across all generated classes:

new Select("#suffix-demo", {
  suffix: "compact",
  options: [{ label: "React", value: "react" }]
});

With default prefix js-select, this adds modifier classes like:

  • js-select--compact
  • js-select__control--compact
  • js-select__option--compact
  • js-select__menu--compact

Migration / Backward Compatibility

  • New recommended package/import naming is vanilla-select-js and Select.
  • Existing runtime/class/component name JSSelect is still exported for compatibility.
  • Core supports both import Select from "vanilla-select-js" and import { Select, JSSelect } from "vanilla-select-js".
  • React adapter supports both import { Select } from "vanilla-select-js/react" and import { JSSelect } from "vanilla-select-js/react".

API

Constructor

new Select(target: string | HTMLElement, config?: JSSelectConfig)

Props (Core JSSelectConfig)

| Prop | Type | Default | Description | | --- | --- | --- | --- | | options | Option[] | [] | Option list (supports nested via children / options). | | value | Value \\| {label,value} \\| Array<Value \\| {label,value}> | undefined | Initial/controlled selected value(s). | | multiple | boolean | false | Enables multi-select mode. | | disabled | boolean | false | Disables interactions. | | searchable | boolean | true | Enables input typing/filtering (react-select isSearchable equivalent). | | closeOnSelect | boolean | !multiple | Closes menu after selection in single mode by default. | | clearable | boolean | true | Shows clear button when value exists. | | menuIsOpen | boolean | undefined | Controlled menu visibility (debug/external control). | | showCheckbox | boolean | false | Shows checkboxes for selectable options in multi mode. | | injectStyles | boolean | true | Auto-injects default stylesheet. | | inputId | string | auto-generated | Input id for external <label for=\"...\"> association. | | name | string | undefined | Input name attribute. | | ariaLabel | string | undefined | Accessible label text. | | ariaLabelledBy | string | undefined | Id of element(s) that label this control. | | ariaDescribedBy | string | undefined | Id of element(s) that describe this control. | | debounce | number | 250 | Search debounce in ms (primarily for async loadOptions). | | className | string | \"\" | Extra root class name(s). | | classNamePrefix | string | \"js-select\" | Class prefix for generated slots. | | classNames | Record<string, string \\| (state)=>string> | {} | Slot-level class overrides. | | suffix | string | null | BEM modifier token applied to root and all slot classes. | | selectableParents | boolean | false | Parent nodes selectable (false keeps toggle-only parents). | | defaultExpanded | boolean | true | Expands tree parents initially. | | messages | { placeholder?, noOptionsText?, loadingText?, clearText? } | defaults | UI message overrides. | | loadOptions | (query, meta) => Promise<Option[]> \\| Option[] | null | Async/sync option loading for search. | | renderOptionLabel | (option, state) => string \\| Node | null | Custom menu option label renderer. | | renderValueLabel | (option, state) => string \\| Node | null | Custom selected value label renderer. | | onChange | (value, detail) => void \\| Promise<void> | noop async | Selection change callback. | | onOpen | (state) => void \\| Promise<void> | noop async | Menu open callback. | | onClose | (state) => void \\| Promise<void> | noop async | Menu close callback. | | onSearch | ({query,total,loading}) => void \\| Promise<void> | undefined | Optional informational callback after search results update. | | onFocus | (state) => void \\| Promise<void> | noop async | Focus callback. | | onBlur | (state) => void \\| Promise<void> | noop async | Blur callback. | | onError | (error) => void \\| Promise<void> | noop async | Error callback. |

Notes:

  • messages.placeholder is shown as decorative text when unfocused, and as input placeholder when focused.
  • With loadOptions, menu-open will fetch only for initial empty data or non-empty query (prevents repeated open calls with already-loaded options).
  • Local filtering works without onSearch.
  • For proper external label support, pass inputId and use <label for=\"that-id\">.

Accessibility Example (label for + ARIA)

<label id="framework-label" for="framework-input">Framework</label>
<p id="framework-help">Type to search frameworks</p>
<div id="framework-select"></div>
<script type="module">
  import Select from "vanilla-select-js";

  new Select("#framework-select", {
    inputId: "framework-input",
    name: "framework",
    ariaLabelledBy: "framework-label",
    ariaDescribedBy: "framework-help",
    options: [
      { label: "React", value: "react" },
      { label: "Vue", value: "vue" }
    ]
  });
</script>

Props (React Adapter JSSelectProps)

JSSelectProps includes all core JSSelectConfig props plus:

| Prop | Type | Description | | --- | --- | --- | | className | string | Wrapper class on the React container element. | | style | React.CSSProperties | Inline style for wrapper container. | | onReady | (instance) => void | Called with created core instance after mount. |

Instance methods

  • open()
  • close()
  • toggleMenu()
  • focus()
  • blur()
  • search(query)
  • setOptions(options)
  • setValue(value)
  • getValue() -> single { label, value } | null, multi Array<{ label, value }>
  • getSelectedOptions()
  • clear()
  • destroy()

Events

The root element dispatches bubbling custom events:

  • js-select:open
  • js-select:close
  • js-select:focus
  • js-select:blur
  • js-select:search
  • js-select:change
  • js-select:error

Contributing

  • Contribution guide: CONTRIBUTING.md
  • Maintainer/developer docs: docs/DEVELOPER_DOCUMENTATION.md

Search Tags / Keywords

Common discovery terms covered by this package and docs:

select, dropdown, multiselect, tree select, nested select, async select, react select,react select alternative, vanilla js select, custom select component, vanilla-select-js.