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

@ozankurt/select3

v1.0.0

Published

Modern, dependency-free <select> enhancer: single/multi, scoring search, tagging, AJAX and a WAI-ARIA combobox. The successor to Select2 / Tom Select / SumoSelect.

Readme

@ozankurt/select3

npm types license no deps

Modern, dependency-free <select> enhancer. No jQuery, no runtime dependencies. A from-scratch successor to Select2 / Tom Select / SumoSelect that progressively enhances a native <select> while keeping it as the single source of truth — so form submission, validation, and reset all keep working.

Written in TypeScript, shipped as tree-shakeable ESM with full type definitions. Works with any bundler (Vite, webpack, Rollup, esbuild).

import { createSelect3 } from '@ozankurt/select3';
import '@ozankurt/select3/css';

const s3 = createSelect3(document.querySelector('select'), {
  placeholder: 'Pick a city…',
  fuzzy: true,
  tags: true,
});

Why select3

| | Select2 | Tom Select | SumoSelect | select3 | |---|---|---|---|---| | Dependencies | jQuery | none | jQuery | none | | TypeScript / ESM | no | yes | no | yes | | Fuzzy search | no | partial | no | yes | | Virtual scroll | no | plugin | no | built-in | | Select-All / OK-Cancel | no | no | yes | yes | | AJAX + pagination | yes | yes | no | yes | | Rich options (icon/image) | templates | templates | no | built-in + templates | | WAI-ARIA combobox | broken | basic | none | full | | Framework adapters | jQuery | vanilla | jQuery | React / Vue / Alpine |


Contents


Install

npm install @ozankurt/select3
import createSelect3 from '@ozankurt/select3';
import '@ozankurt/select3/css';

Quick start

const s3 = createSelect3(document.querySelector('select'), { placeholder: 'Pick a city…' });
// later
s3.destroy();

The native <select> stays in the DOM and in sync. Configure via the options argument or data-* attributes on the element (the data attribute wins):

<select data-select3 data-placeholder="Pick a city…" data-tags data-ajax="/cities">
  <option value="ams">Amsterdam</option>
</select>

Per-<option> rich data: data-icon, data-image, data-description, data-badge (and any data-* you want to make searchable via searchField).


Options reference

All options are optional. Pass them as the second argument to createSelect3(el, options).

Core

| Option | Type | Default | Description | |---|---|---|---| | placeholder | string | locale | Empty-state text (data-placeholder). | | allowClear | boolean | true | Show the clear (×) control. Suppressed for a required single-select. | | searchable | boolean | true | Show the search input. | | closeOnSelect | boolean | !multiple | Close the dropdown after a pick. | | openOnFocus | boolean | false | Open when the control gains focus. | | hidePlaceholder | boolean | false | Hide the placeholder while open. | | maxItems | number \| null | null | Cap selections in multi-select. | | selectOnTab | boolean | false | Tab selects the active option. |

Search

| Option | Type | Default | Description | |---|---|---|---| | searchField | string[] | ['label'] | Fields to match ('label', 'group', or any data-* key). | | fuzzy | boolean | false | Also match an in-order subsequence (typo/abbreviation tolerant). | | searchGroups | boolean | false | Include the optgroup label in the haystack. | | sort | boolean \| (a,b) => number | true | Rank by relevance, keep original order (false), or a custom comparator. | | highlight | boolean | true | Wrap matched text in <mark>. | | minResultsForSearch | number | 0 | Hide the search box below N options. | | maxInputLength | number | 0 | Max chars in the search input (0 = no limit). | | maxRender | number | 100 | Cap rendered rows (see virtualScroll for huge lists). |

Tagging

| Option | Type | Default | Description | |---|---|---|---| | tags | boolean | false | Allow creating options by typing (data-tags). | | tokenSeparators | string[] | [] | Auto-create a tag when one of these is typed/pasted. | | createFilter | RegExp \| string \| (s) => boolean | — | Validate a new tag before creating it. | | persist | boolean | true | Keep created options after deselect. | | addPrecedence | boolean | false | Enter creates the tag instead of picking a match. | | createText | (q) => string | locale | Label for the "create" row. | | create | (label) => option \| null \| false \| Promise<…> | — | Custom/async tag creation (e.g. persist server-side, then select). |

Data / AJAX

| Option | Type | Default | Description | |---|---|---|---| | ajax | string \| null | null | Remote endpoint (data-ajax). | | minChars | number | 1 ajax / 0 | Chars before searching. | | debounce | number | 250 | Debounce (ms) for remote search. | | preload | boolean \| 'focus' | false | Load page 1 on init or first focus. | | queryParam / pageParam | string | 'q' / 'page' | Request param names. | | processResults | (raw, page) => {results, hasMore} | default | Map any server shape. | | transport | (url, {signal}) => Promise | fetch | Swap the network layer. | | cacheTTL | number | 0 | ms before cached results are served stale and revalidated (SWR). 0 = forever. |

Multi-select UX

| Option | Type | Default | Description | |---|---|---|---| | selectAll | boolean | false | Select-All / Clear-All header (with indeterminate). | | checkboxes | boolean | false | Checkbox affordance per option. | | okCancel | boolean | false | Stage edits; commit on OK, revert on Cancel. | | clickAway | 'commit' \| 'revert' | 'commit' | OK/Cancel behaviour on outside click. | | csvDispCount | number | 0 | Collapse tags into an "N selected" caption past N. | | captionFormat / captionFormatAllSelected | (n[, total]) => string | locale | Caption text. | | captionPrefix | string | '' | Text prepended to the caption. |

Display, theming & behaviour

| Option | Type | Default | Description | |---|---|---|---| | templates | Select3Templates | {} | Custom render slots (see below). | | theme | string \| null | null | Apply a preset: 'bootstrap4', 'bootstrap5', 'tailwind'. | | size | 'sm' \| 'md' \| 'lg' | 'md' | Control density. | | rtl | boolean | auto | Right-to-left (auto-detected from [dir=rtl]). | | classNames | { wrap, control, dropdown, listbox, option, tag, search } | {} | Inject your own classes. | | appendTo | 'auto' \| 'self' \| 'body' \| HTMLElement | 'auto' | Dropdown parent (see modals). | | dropdownGap / dropdownZ | number | 4 / 1100 | Gap + z-index of the portaled dropdown. | | virtualScroll | boolean | false | Window large lists. | | itemHeight | number | 36 | Row height for virtual scroll. | | nativeOnMobile | boolean \| 'auto' | 'auto' | Use the OS picker on touch devices. | | recent | boolean \| { key?, max? } | — | Float recently-chosen values to the top (localStorage-backed). | | showCount / countText | boolean / (n) => string | false | Result-count footer row. | | i18n / locale | Partial<Locale> | — | Per-instance translations. | | plugins | (string \| factory \| {name, options})[] | [] | Attach plugins. |


Search

createSelect3(el, {
  fuzzy: true,                      // "amtrdm" matches "Amsterdam"
  searchGroups: true,              // also match the optgroup label
  searchField: ['label', 'iso'],  // match label + a data-iso attribute
});

Independent display vs search: point searchField at data-* fields and the visible label is never matched — show AU, search Australia:

<option value="au" data-search="Australia">AU</option>
createSelect3(el, { searchField: ['search'] });

Tagging

createSelect3(el, {
  tags: true,
  tokenSeparators: [',', ' '],     // typing/pasting splits into tags
  createFilter: /^[\w.+-]+@[\w-]+\.\w+$/, // only valid emails
  persist: false,                  // drop created tags on deselect
});

AJAX

The endpoint receives ?q= (and ?page= for pagination) and returns either a bare array or { results, hasMore }:

[{ "value": "ams", "label": "Amsterdam", "icon": "fi fi-nl", "group": "Europe" }]

Requests are debounced, de-duplicated, cached, and aborted when superseded. Remote rows may carry icon / image / description / badge (rendered on the option) and a group (placed into the matching <optgroup>). Map any other server shape with processResults.

Rich option data & templates

Icons/images/descriptions/badges render by default from data-* (or array/AJAX fields) — no template needed:

<option value="au" data-icon="fi fi-au" data-description="Oceania" data-badge="Popular">Australia</option>

Override any slot. Return a string (inserted as safe text) or a DOM Node (inserted as-is). select3 never uses innerHTML.

createSelect3(el, {
  templates: {
    option: (item, { escape }) => { const n = document.createElement('span'); n.textContent = item.label; return n; },
    item: (item) => `★ ${item.label}`,
    optgroupHeader: (group) => group.toUpperCase(),
    noResults: (q) => `No match for “${q}”`,
    caption: (n, total) => `${n} of ${total}`,
    create: (q) => `Add “${q}”`,
    loading: () => 'Searching…',
    loadingMore: () => 'Loading more…',
  },
});

Multi-select UX

createSelect3(el, {
  selectAll: true,        // Select-All / Clear-All with indeterminate state
  checkboxes: true,       // checkbox per option
  okCancel: true,         // stage edits, commit on OK
  csvDispCount: 3,        // show 3 tags, then "N selected"
  maxItems: 5,
});

Theming & styling

  • CSS variables — override --color-* (and --select3-dropdown-z) on .s3 or any ancestor.
  • classNames — inject your own classes on each part.
  • Theme presets — load one CSS file, then opt in per instance:
import '@ozankurt/select3/themes/bootstrap5'; // or bootstrap4, tailwind
createSelect3(el, { theme: 'bootstrap5' });    // adds .s3--theme-bootstrap5
  • size (sm/md/lg) and rtl (auto from [dir=rtl]).

Modal compatibility

Works inside Bootstrap 4/5/6 modals and Tailwind/Alpine (x-trap) modals. With the default appendTo: 'auto' the dropdown is portaled into the nearest modal ancestor, so focus traps don't steal the search box and overflow:hidden can't clip it. Override with appendTo: 'body' | element or dropdownParent.

Virtual scroll & native mobile

createSelect3(el, { virtualScroll: true, itemHeight: 36 }); // smooth at 100k+ options
createSelect3(el, { nativeOnMobile: 'auto' });              // OS picker on touch (default)

Events

instance.on(event, handler) returns an unsubscribe function; once and off are also available. The native <select> still fires change for forms.

| Event | Payload | Fires when | |---|---|---| | change | { value } | Selection committed | | select / deselect | { value } | An option is (de)selected | | clear | — | Cleared | | open / close | — | Dropdown toggled | | type | { query } | User typed in the search box | | optionAdd / optionRemove | { value } | Option set changed | | okCancel:ok / okCancel:cancel | — | OK/Cancel staging resolved | | destroy | — | Instance torn down |

Cancelable beforeOpen / beforeSelect / beforeClear receive a preventDefault.

Methods

open() · close() · toggle()
choose(value) · deselect(value) · clearAll() · selectAll()
getValue(): string | string[] · setValue(value)
addOption({value,label,selected?,disabled?}) · addOptions([...]) · removeOption(value) · getOption(value) · clearOptions()
enable() · disable() · lock() · unlock() · isFull()
createTag(label) · confirm() · cancel()         // tagging · OK-Cancel
focus() · blur() · sync() · refresh() · destroy()
on(event, fn) · once(event, fn) · off(event, fn?)

Localization

import { setLocale } from '@ozankurt/select3';
import { tr } from '@ozankurt/select3/i18n/tr';   // ships en, es, fr, de, tr

setLocale(tr);                       // global default
createSelect3(el, { locale: tr });   // per-instance (wins)

Every user-facing string is translatable via the Locale interface.

Plugins

import { registerPlugin } from '@ozankurt/select3';
import '@ozankurt/select3/plugins/drag-drop';
import '@ozankurt/select3/plugins/drag-drop.css';

createSelect3(el, {
  multiple: true,
  plugins: ['drag-drop', { name: 'csv-output', options: { separator: ';' } }],
});

Built-ins: drag-drop (reorder tags), csv-output (mirror to a hidden comma-joined input), optgroup-columns (grid layout). Write your own — a factory (instance, options?) => ({ destroy?() }) using the public API/events.

Framework adapters

React / Vue / Alpine are optional peer dependencies; each is a subpath export.

import Select3React from '@ozankurt/select3/react';
import { useSelect3, Select3Vue } from '@ozankurt/select3/vue';
import select3Plugin from '@ozankurt/select3/alpine'; // Alpine.plugin(select3Plugin)

Accessibility

The focused search input is the combobox (aria-expanded / aria-controls / aria-activedescendant); the listbox and options are labelled, and the accessible name resolves from the associated <label> (or aria-label). Keyboard: arrows move the active option (skipping disabled rows), Enter selects, Escape closes, Backspace removes the last tag, Ctrl+A selects all (multi).

License

MIT © Ozan Kurt