npm package: @h7o/searchable-select
Description: A framework-agnostic searchable select dropdown component — zero dependencies. Self-contained JS + CSS that works anywhere.
Author: Hussein Al Bayati
Latest version: 1.0.0

# searchable-select

A framework-agnostic searchable select dropdown — **zero dependencies**. No Bootstrap, no React, no jQuery. Self-contained JS + CSS that works anywhere.

![Vanilla JS](https://img.shields.io/badge/Vanilla-JS-F7DF1E?logo=javascript&logoColor=black)
![Zero Dependencies](https://img.shields.io/badge/Dependencies-0-brightgreen)
![License](https://img.shields.io/badge/License-MIT-green)

> **Part of the SearchableSelect family:**
> - [`searchable-select`](https://github.com/H7O/searchable-select) — Framework-agnostic (this package)
> - [`bs-searchable-select`](https://github.com/H7O/bs-searchable-select) — Bootstrap 5
> - [`react-bs-searchable-select`](https://github.com/H7O/react-bs-searchable-select) — React + Bootstrap

## Features

- **Search & Filter** — Case-insensitive substring matching across all option labels
- **Custom Values** — Optionally allow users to type values not in the list
- **Clearable** — One-click clear button (can be disabled)
- **Keyboard Navigation** — Arrow keys, Enter to select, Escape to close, Tab to close
- **Programmatic API** — `getValue()`, `setValue()`, `addOption()`, `removeOption()`, `clearSelection()`, and more
- **Data-Attribute Init** — Configure entirely in HTML with `data-bss-*` attributes
- **Custom Events** — `bss.change`, `bss.show`, `bss.hide`, `bss.clear` on the container element
- **Styling** — Inline colour props, CSS class overrides, or CSS custom properties (variables)
- **Validation** — Built-in `.bss-invalid` support for form validation
- **Dark Mode** — `data-bss-theme="dark"`, `bss-dark` class, or Bootstrap's `data-bs-theme="dark"`
- **Theming** — Full control via CSS custom properties — colours, borders, fonts, radii, shadows
- **Accessible** — ARIA `combobox` / `listbox` roles, keyboard support
- **Zero Dependencies** — Ships its own complete CSS, works in any environment
- **UMD Module** — Works with `<script>` tags, CommonJS (`require`), and AMD (`define`)
- **ES5 Compatible** — No transpilation needed, works in older browsers

## Installation

### CDN (quickest)

```html
<!-- CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/searchable-select@1/dist/searchable-select.min.css">

<!-- JS -->
<script src="https://cdn.jsdelivr.net/npm/searchable-select@1/dist/searchable-select.min.js"></script>
```

### npm

```bash
npm install searchable-select
```

```js
// ES module
import SearchableSelect from 'searchable-select';
import 'searchable-select/dist/searchable-select.css';

// CommonJS
const SearchableSelect = require('searchable-select');
```

### Manual

Copy `dist/searchable-select.min.js` and `dist/searchable-select.min.css` into your project and include them in your HTML.

## Quick Start

### JavaScript

```html
<link rel="stylesheet" href="searchable-select.min.css">
<script src="searchable-select.min.js"></script>

<div id="my-select"></div>

<script>
var select = new SearchableSelect('#my-select', {
    options: [
        { value: 'apple',  label: 'Apple' },
        { value: 'banana', label: 'Banana' },
        { value: 'cherry', label: 'Cherry' }
    ],
    placeholder: 'Select a fruit...',
    onChange: function (value, option) {
        console.log('Selected:', value);
    }
});
</script>
```

### Data Attributes (no JS required)

```html
<div data-bss-toggle="searchable-select"
     data-bss-placeholder="Pick a fruit..."
     data-bss-options='[
         {"value":"apple","label":"Apple"},
         {"value":"banana","label":"Banana"},
         {"value":"cherry","label":"Cherry"}
     ]'>
</div>
```

Elements with `data-bss-toggle="searchable-select"` are automatically initialised on `DOMContentLoaded`. Listen for events via the DOM:

```js
document.getElementById('my-select')
    .addEventListener('bss.change', function (e) {
        console.log('Selected:', e.detail.value);
    });
```

## Configuration

Pass these as the second argument to `new SearchableSelect(el, config)`, or as `data-bss-*` attributes in HTML.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `options` | `Array<{ value, label }>` | `[]` | Array of selectable options |
| `value` | `any` | `null` | Initially selected value |
| `placeholder` | `string` | `'Select...'` | Placeholder text |
| `maxDropdownHeight` | `number` | `200` | Max dropdown height in pixels |
| `allowCustomValue` | `boolean` | `false` | Allow values not in the options list |
| `disabled` | `boolean` | `false` | Disable the component |
| `clearable` | `boolean` | `true` | Show/hide the clear (×) button |
| `invalid` | `boolean` | `false` | Apply `.bss-invalid` styling |
| `activeBackgroundColor` | `string` | — | Background colour of the selected item |
| `activeTextColor` | `string` | — | Text colour of the selected item |
| `activeClassName` | `string` | — | CSS class for the selected item (overrides inline styles) |
| `inactiveBackgroundColor` | `string` | — | Background colour for unselected items |
| `inactiveTextColor` | `string` | — | Text colour for unselected items |
| `inactiveClassName` | `string` | — | CSS class for unselected items (overrides inline styles) |
| `onChange` | `function(value, option)` | — | Callback on selection change; receives `(null, null)` on clear |

### Data-attribute mapping

Prefix every option with `data-bss-` and use kebab-case:

```html
<div data-bss-toggle="searchable-select"
     data-bss-placeholder="Pick…"
     data-bss-allow-custom-value="true"
     data-bss-clearable="false"
     data-bss-max-dropdown-height="300"
     data-bss-options='[{"value":"a","label":"A"}]'>
</div>
```

## Methods

All methods are available on the instance returned by `new SearchableSelect(...)`.

| Method | Returns | Description |
|--------|---------|-------------|
| `getValue()` | `any` | Current value (selected option value or custom text) |
| `setValue(value)` | `void` | Programmatically select a value |
| `isCustomValue()` | `boolean` | `true` if the current value is not from the options list |
| `getInputText()` | `string` | Raw text currently shown in the input |
| `clearSelection()` | `void` | Clear the selection |
| `addOption({ value, label })` | `void` | Add an option (duplicates ignored) |
| `removeOption(value)` | `void` | Remove an option; clears selection if it was active |
| `setOptions(options)` | `void` | Replace the entire options list |
| `getOptions()` | `Array` | Get a copy of the current options list |
| `enable()` | `void` | Enable the component |
| `disable()` | `void` | Disable the component |
| `setInvalid(bool)` | `void` | Toggle `.bss-invalid` styling |
| `dispose()` | `void` | Tear down: unbind events, remove DOM, clean up |

### Static Methods

| Method | Description |
|--------|-------------|
| `SearchableSelect.initAll()` | Initialise all `[data-bss-toggle="searchable-select"]` elements |
| `SearchableSelect.getInstance(el)` | Retrieve existing instance from a container element |
| `SearchableSelect.fromDataAttributes(el)` | Create instance from an element's `data-bss-*` attributes |

## Events

Custom DOM events are dispatched on the container element:

| Event | `e.detail` | Description |
|-------|------------|-------------|
| `bss.change` | `{ value, option }` | Selection changed |
| `bss.show` | `{}` | Dropdown opened |
| `bss.hide` | `{}` | Dropdown closed |
| `bss.clear` | `{}` | Selection cleared |

```js
document.querySelector('#my-select')
    .addEventListener('bss.change', function (e) {
        console.log('New value:', e.detail.value);
        console.log('Option object:', e.detail.option); // null if custom
    });
```

## Theming with CSS Custom Properties

Every visual aspect can be customised via CSS variables on `.bss-container`:

```css
.bss-container {
    /* Input */
    --bss-input-bg: #fff;
    --bss-input-color: #212529;
    --bss-input-border: #ced4da;
    --bss-input-border-radius: 0.375rem;
    --bss-input-focus-border: #86b7fe;
    --bss-input-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
    --bss-input-placeholder-color: #6c757d;

    /* Dropdown */
    --bss-dropdown-bg: #fff;
    --bss-dropdown-border: #dee2e6;
    --bss-dropdown-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
    --bss-dropdown-radius: 0.375rem;

    /* Options */
    --bss-option-active-bg: #0d6efd;
    --bss-option-active-color: #fff;
    --bss-option-hover-bg: #f8f9fa;
    --bss-option-focused-bg: #e9ecef;

    /* Typography */
    --bss-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    --bss-font-size: 0.9375rem;
}
```

## Dark Mode

Enable dark mode in any of these ways:

```html
<!-- On any ancestor element -->
<html data-bss-theme="dark">

<!-- On the container itself -->
<div id="select" class="bss-dark"></div>

<!-- Also works with Bootstrap's data-bs-theme -->
<html data-bs-theme="dark">
```

## Migrating from Chosen

If you're replacing the jQuery Chosen plugin:

1. Remove Chosen JS and CSS files
2. Include `searchable-select.min.js` and `searchable-select.min.css`
3. Replace `<select>` elements with `<div>` containers
4. Initialise with `new SearchableSelect(el, { options: [...] })`
5. Replace Chosen events with `bss.change`, `bss.show`, `bss.hide`

```js
// Before (Chosen)
$('#my-select').chosen({ allow_single_deselect: true });
$('#my-select').on('change', function () { ... });

// After (SearchableSelect)
var select = new SearchableSelect('#my-select', {
    options: [...],
    clearable: true,
    onChange: function (value) { ... }
});
```

## Migrating from bs-searchable-select

The API is identical — just change the constructor name:

```js
// Before
var select = new BsSearchableSelect('#el', config);

// After
var select = new SearchableSelect('#el', config);
```

Both use the same `bss-` CSS prefix, same data attributes, same events, and same methods.

## Examples

### Custom Values

```js
var select = new SearchableSelect('#el', {
    options: myOptions,
    allowCustomValue: true,
    placeholder: 'Select or type…'
});

select.getValue();       // the selected/typed value
select.isCustomValue();  // true if typed something not in the list
```

### Inline Colour Styling

```js
new SearchableSelect('#el', {
    options: myOptions,
    activeBackgroundColor: '#198754',
    activeTextColor: '#fff',
    inactiveBackgroundColor: '#f8f9fa',
    inactiveTextColor: '#333'
});
```

### CSS Class Styling

```js
new SearchableSelect('#el', {
    options: myOptions,
    activeClassName: 'my-active',
    inactiveClassName: 'my-inactive'
});
```

```css
.my-active {
    background-color: #6f42c1 !important;
    color: #fff !important;
    border-left: 3px solid #4a2a8a;
}
.my-inactive:hover {
    background-color: #e9ecef;
}
```

### Form Validation

```js
var select = new SearchableSelect('#el', {
    options: myOptions,
    invalid: true
});

document.querySelector('#el').addEventListener('bss.change', function (e) {
    if (e.detail.value) select.setInvalid(false);
});
```

### Dynamic Options

```js
var select = new SearchableSelect('#el', { options: [] });

fetch('/api/items')
    .then(function (r) { return r.json(); })
    .then(function (data) {
        select.setOptions(data.map(function (d) {
            return { value: d.id, label: d.name };
        }));
    });
```

## Demo

Open `index.html` in a browser to see all features in action — no build step required, no CDN dependencies.

```bash
npx serve .
```

## Building

To generate minified dist files:

```bash
npm install
npm run build
```

This produces:
- `dist/searchable-select.js` — unminified copy
- `dist/searchable-select.min.js` — minified
- `dist/searchable-select.css` — unminified copy
- `dist/searchable-select.min.css` — minified

## License

[MIT](LICENSE) © Hussein Al Bayati