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

kselect.js

v1.4.4

Published

A modern, accessible select replacement — single file, no dependencies.

Downloads

252

Readme

kselect.js

A modern, accessible select replacement - single file, no dependencies.

Demo at https://kastrack.github.io/kselect.js/

kselect.js progressively enhances native <select> elements with live search, multi-select tags, collapsible optgroups, selection limits, HTML option labels, and a mobile bottom-sheet modal. It writes all changes back to the original <select>, so it works seamlessly with any form or framework.


Features

  • Searchable - live filtering as you type; optgroups auto-expand on match
  • Single & multi select - both modes supported, auto-detected from the <select> element
  • Tag-style multi select - selected values appear as removable inline pills
  • Checkbox list - dropdown items show checkboxes in multi-select mode
  • Collapsible optgroups - click a group header to collapse or expand it
  • Select all - global and per-group "select all" controls in multi-select mode
  • Selection limit - cap how many items can be selected with maxSelect
  • HTML option labels - opt in with allowHtml: true to render rich markup inside option labels
  • Mobile bottom-sheet - on phones, opens a full-screen bottom-sheet modal instead of a dropdown
  • Native picker fallback - optionally use the OS picker on touch devices with nativeOnMobile
  • Syncs the native <select> - all selections are reflected back to the real element
  • Native change event - fires on the original <select> so existing listeners and frameworks work without changes
  • Custom events - kselect:change, kselect:open, kselect:close
  • Keyboard accessible - Arrow keys, Enter, Escape, and Tab navigation fully supported
  • Screen reader friendly - WCAG 2.1 AA compliant with full ARIA attributes
  • Themeable - ~40 CSS custom properties; 20 ready-made themes included
  • Lightweight - single .js + single .css, minified versions included, zero dependencies

Installation

Download kselect.min.js and kselect.min.css (or the unminified versions) and add them to your page:

<link rel="stylesheet" href="kselect.min.css">
<script src="kselect.min.js"></script>

kselect.js also supports CommonJS:

const Kselect = require('./kselect.js');

Quick Start

<select id="my-select">
  <option value="js">JavaScript</option>
  <option value="py">Python</option>
  <option value="go">Go</option>
</select>

<script>
  const [ks] = Kselect.init('#my-select');
</script>

That's it. kselect hides the original <select> and inserts its own widget immediately after it. Your form submission, validation, and any existing event listeners continue to work unchanged.


Usage

Initialise one element

const [ks] = Kselect.init('#my-select');
// or:
const ks = Kselect.init('#my-select')[0];

Initialise multiple elements at once

const all = Kselect.init('select');
const all = Kselect.init('.my-selects');

Kselect.init always returns an array of instances. Use [0] or destructuring to get a single instance.

Retrieve an existing instance

const ks = Kselect.getInstance(document.getElementById('my-select'));

Options

Kselect.init('#my-select', {
  placeholder:       'Choose an option…',
  searchPlaceholder: 'Search…',
  noResultsText:     'No results found',
  maxHeight:         300,
  searchable:        true,
  allowClear:        true,
  closeOnSelect:     true,
  collapseGroups:    false,
  selectAll:         false,
  selectAllText:     'Select all',
  selectAllGroups:   false,
  nativeOnMobile:    false,
  mobileModal:       true,
  maxSelect:         null,
  maxSelectText:     'Max {n} items',
  allowHtml:         false,
  showEmptyOptGroups: true,
});

Options can also be set via data- attributes on the <select> element:

<select data-placeholder="Pick a country…" data-max-height="400">…</select>

Full options reference

| Option | Type | Default | Description | |---|---|---|---| | placeholder | string | 'Select an option…' | Text shown when nothing is selected | | searchPlaceholder | string | 'Search…' | Placeholder inside the search input | | noResultsText | string | 'No results found' | Shown when search returns nothing | | maxHeight | number | 300 | Maximum height of the dropdown in px | | searchable | boolean | true | Show or hide the search input | | allowClear | boolean | true | Show a clear (×) button | | closeOnSelect | boolean | true | Close after picking in single mode | | collapseGroups | boolean | false | Start optgroups collapsed | | selectAll | boolean | false | Show a global "Select all" row (multi only) | | selectAllText | string | 'Select all' | Label for the global select-all row | | selectAllGroups | boolean | false | Show a select-all checkbox in each optgroup header (multi only) | | selectAllGroupText | string | 'Select all' | Accessible label for per-group select-all buttons | | nativeOnMobile | boolean | false | Use the native OS picker on coarse-pointer (touch) devices | | mobileModal | boolean | true | Show a bottom-sheet modal on phones instead of a dropdown | | maxSelect | number|null | null | Maximum items selectable (multi only; null = unlimited) | | maxSelectText | string | 'Max {n} items' | Notice text when the selection limit is reached; {n} = the limit | | allowHtml | boolean | false | Render HTML markup in option labels (see below). Off by default — opt in only when option text is trusted | | summarizeSelected | 'auto'|'off'|false|number | 'auto' | Multi only. 'auto' = collapse to a "{n} selected" summary when tags would wrap to a second line; 'off' or false = always show all tags; number n = collapse when count exceeds n | | summarizeSelectedText | string | '{n} selected' | Template for the collapsed-summary text; {n} = the count of selected items | | autoSync | boolean | true | Watch the underlying <select> for external mutations and re-render automatically. Set false to manage syncing yourself via refresh() / kselect:sync | | showEmptyOptGroups | boolean | true | Whether empty <optgroup> headers (groups with no <option> children) appear in the dropdown. true always shows the header; false omits the optgroup entirely |


Instance API

| Method | Description | |---|---| | getValue() | Returns the current value - string (single) or string array (multi) | | setValue(v) | Set selection by value string or array; fires change | | clear() | Deselect all; fires change | | open() | Open the dropdown | | close() | Close the dropdown | | enable() | Enable the control | | disable() | Disable the control | | refresh() | Rebuild option list from the native <select> DOM | | destroy() | Remove the widget and restore the original <select> |


Events

kselect fires events on the original <select> element.

| Event | When | |---|---| | change | A selection changes (native event) | | kselect:change | A selection changes (custom event) | | kselect:open | The dropdown opens | | kselect:close | The dropdown closes | | kselect:sync | Dispatch this to force kselect to re-read the <select> DOM |

const selectEl = document.getElementById('my-select');

selectEl.addEventListener('change', () => {
  console.log('Value:', ks.getValue());
});

Every event kselect dispatches itself carries an event.kselect === true flag, so listeners can tell its events apart from external mutations of the same <select>:

selectEl.addEventListener('change', (e) => {
  if (e.kselect) return; // ignore kselect's own changes
  // …handle external change
});

Auto-sync with the underlying <select>

With the default autoSync: true, kselect watches the underlying <select> for external mutations (options added/removed, attribute or label edits, programmatic value assignment) and re-renders automatically — you do not need to call refresh() or dispatch kselect:sync after typical updates:

// Add an option after init — kselect picks it up on its own
const opt = document.createElement('option');
opt.text = 'Rust';
selectEl.appendChild(opt);

// Set the value programmatically — kselect picks it up too, provided the change
// is dispatched as a native event
selectEl.value = 'ts';
selectEl.dispatchEvent(new Event('change'));

refresh() and kselect:sync are still available for autoSync: false setups and for forcing an immediate resync.

jQuery .trigger("change") caveat

jQuery's .trigger("change") does not dispatch a real DOM event — it walks jQuery's own handler queue and stops there. Native addEventListener('change', …) listeners (including kselect's auto-sync hook) are never called. Combined with jQuery's .val(…) and .prop('selected', …) — which mutate the selected IDL property, not the attribute, so a MutationObserver does not see them either — this means kselect can miss programmatic updates made entirely through jQuery.

If you are driving the <select> from jQuery, either dispatch a native event after the mutation, or call refresh() directly:

$('#my-select').val('ts');
document.getElementById('my-select')
        .dispatchEvent(new Event('change'));   // native — kselect picks it up

// or
$('#my-select').val('ts');
ks.refresh();

Per-row styling via class and style

Set class or style on any <option> or <optgroup> and kselect carries the attributes onto the rendered chrome — option rows, group wrappers/headers/lists, and selected-state tags (or the single-value span in single mode). This is the recommended way to attach per-row styling hooks without writing post-render mutation code.

Use style for values that vary per row and come from your data (DB-derived colours, count badges, dates) — style="--chip-color: …" is the idiomatic carrier, with project CSS reading var(--chip-color). Use class for enumerable states known when the CSS is written (is-premium, is-deprecated).

<select multiple>
  <optgroup label="Rock" style="--group-color: #b53f5c">
    <option value="rock" style="--chip-color: #b53f5c">Rock</option>
    <option value="punk" style="--chip-color: #d44a2a" class="is-recommended">Punk Rock</option>
  </optgroup>
  <optgroup label="Jazz" style="--group-color: #5a4cb5">
    <option value="jazz" style="--chip-color: #5a4cb5">Jazz</option>
  </optgroup>
</select>
.ks-tag,
.ks-option {
  --chip-color: currentColor;
}
.ks-tag {
  background:   color-mix(in srgb, var(--chip-color) 18%, transparent);
  color:        var(--chip-color);
  border-color: color-mix(in srgb, var(--chip-color) 50%, transparent);
}
.ks-option { border-left: 3px solid var(--chip-color); }

.ks-group-header { color: var(--group-color); }

.ks-option.is-recommended { font-weight: 700; }
.ks-option.is-deprecated  { opacity: 0.5; text-decoration: line-through; }

Framework classes (ks-option, ks-tag, ks-group-header, …) are preserved — your classes are appended, not substituted. The carry-through does not apply in nativeOnMobile: true mode (the OS owns the native picker).


Optgroups

kselect renders <optgroup> elements with collapsible headers. Groups auto-expand when a search query matches options within them.

<select id="languages" multiple>
  <optgroup label="Frontend">
    <option value="js">JavaScript</option>
    <option value="ts">TypeScript</option>
  </optgroup>
  <optgroup label="Backend">
    <option value="go">Go</option>
    <option value="rust">Rust</option>
  </optgroup>
</select>

Selection Limit

Kselect.init('#toppings', {
  maxSelect:     3,
  maxSelectText: 'Max {n} toppings',
});

When the limit is reached, unselected options dim, an amber badge appears in the control, and a notice banner appears at the top of the dropdown.


HTML Option Labels

By default, allowHtml: false — tag characters are shown literally so untrusted or arbitrary option text can't inject markup.

Set allowHtml: true to render markup. Escape your HTML as entities in the option text — kselect decodes and renders it:

<option value="bold">&lt;strong&gt;Bold&lt;/strong&gt;</option>
<option value="status">Server &lt;span style="color:green"&gt;● online&lt;/span&gt;</option>

Only enable when the option text is trusted (authored by you, not derived from user input).


Mobile

On phones (coarse-pointer, ≤ 640 px wide), kselect opens a full-screen bottom-sheet modal by default. Disable with mobileModal: false.

For a full native OS picker fallback on all touch devices, use nativeOnMobile: true. The complete instance API still works in this mode.


Theming

Override CSS custom properties on .ks-wrapper, a parent element, or :root:

.my-theme {
  --ks-color-border-focus:       #10b981;
  --ks-color-option-selected:    #10b981;
  --ks-color-option-selected-bg: #ecfdf5;
  --ks-color-tag-bg:             #ecfdf5;
  --ks-color-tag-text:           #059669;
  --ks-color-checkbox-checked:   #10b981;
}

20 ready-made themes are included in the themes/ directory.


Accessibility

WCAG 2.1 AA compliant - role="combobox", full ARIA labelling, aria-live announcements, :focus-visible outlines on all interactive elements, and contrast-checked default colours.


Browser Support

Chrome / Edge 80+, Firefox 75+, Safari 13+, iOS Safari 13+, Chrome for Android. Internet Explorer is not supported.


Contributing

Issues and pull requests are welcome. Please open an issue before starting significant work so we can discuss the approach.


License

MIT - see LICENSE for details.