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

@cliqthemes/glide-dropdown

v0.4.0

Published

A tiny, dependency-free, accessible select/dropdown component. A modern replacement for Select2 / Tom Select.

Readme

Glide

A tiny, dependency-free, accessible select/dropdown component for the web. A modern replacement for Select2: ~9 KB compressed, virtualized to handle 20,000+ options smoothly, and built on plain ES modules — no jQuery, no framework.

Live demo →

  • 🪶 Tiny — core is ~9 KB compressed, zero runtime dependencies
  • ⚡ Fast — virtualized option list; renders a bounded DOM window regardless of dataset size
  • ♿ Accessible — full ARIA combobox pattern, keyboard-only operable, screen-reader announcements
  • 🎨 Themeable — every color/spacing value is a CSS variable; ships light + dark themes
  • 🔌 Extensible — a small plugin API instead of a kitchen-sink core
  • 📦 Modern data formats — upgrades a native <select>, or accepts arrays / grouped arrays / async data

Installation

npm install @cliqthemes/glide-dropdown
import { Glide } from '@cliqthemes/glide-dropdown';
import '@cliqthemes/glide-dropdown/style.css';

Or drop in the UMD build directly — no bundler, no ES modules, no build step. This is the one to use in a WordPress theme/plugin, a CMS template, or any plain HTML page:

<link rel="stylesheet" href="https://unpkg.com/@cliqthemes/glide-dropdown/dist/glide.css" />
<script src="https://unpkg.com/@cliqthemes/glide-dropdown/dist/glide.umd.cjs"></script>
<script>
  const select = new Glide('#country');
</script>

window.Glide is the constructor itself (not Glide.Glide or similar), and every built-in plugin ships attached at Glide.plugins.* in this build — there's no separate plugin file to load:

<script>
  const select = new Glide('#skills', {
    multiple: true,
    plugins: [Glide.plugins.clearButton(), Glide.plugins.checkboxSelection()],
  });
</script>

WordPress

Same two files, enqueued the normal WordPress way instead of linked directly — host them yourself (e.g. copy dist/glide.umd.cjs and dist/glide.css into your theme) rather than pointing production at unpkg:

function my_theme_enqueue_glide() {
    wp_enqueue_style('glide', get_template_directory_uri() . '/assets/glide.css', [], '0.1.0');
    wp_enqueue_script('glide', get_template_directory_uri() . '/assets/glide.umd.cjs', [], '0.1.0', true);
}
add_action('wp_enqueue_scripts', 'my_theme_enqueue_glide');

Then initialize it in your own script (enqueued with 'glide' as a dependency so it loads after), same API as everywhere else:

document.addEventListener('DOMContentLoaded', () => {
  new Glide('#my-select');
});

CDN

Every version published to npm is automatically mirrored on jsDelivr and unpkg — nothing to configure, no separate publish step:

<!-- jsDelivr -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@cliqthemes/glide-dropdown/dist/glide.css" />
<script src="https://cdn.jsdelivr.net/npm/@cliqthemes/glide-dropdown/dist/glide.umd.cjs"></script>

<!-- unpkg -->
<link rel="stylesheet" href="https://unpkg.com/@cliqthemes/glide-dropdown/dist/glide.css" />
<script src="https://unpkg.com/@cliqthemes/glide-dropdown/dist/glide.umd.cjs"></script>

Pin an exact version for production (@cliqthemes/[email protected]/...) rather than the unpinned URLs above, which always resolve to latest — convenient for docs/demos, riskier for something you don't control the timing of.

Quick start

<select id="country">
  <option value="">Choose a country</option>
  <option value="au">Australia</option>
  <option value="nz">New Zealand</option>
</select>
const select = new Glide('#country', {
  searchable: true,
});

select.on('change', ({ value }) => console.log(value));

Multi-select works the same way — Glide reads multiple straight off the <select>:

<select id="skills" multiple>
  <option value="js">JavaScript</option>
  <option value="ts">TypeScript</option>
</select>
new Glide('#skills', { fuzzy: true });

You don't need a <select> at all — pass data directly and mount on any element:

new Glide('#assignee', {
  placeholder: 'Assign to…',
  options: [
    { value: 1, label: 'Grace Hopper' },
    { value: 2, label: 'Ada Lovelace' },
  ],
});

See it live at cliqthemes.github.io/glide-dropdown, or run it locally from examples/index.html (single/multi select, groups, creatable tags, custom rendering, async loading with infinite scroll, and a 20,000-option virtualization stress test) — npm run dev and open it.

Data formats

// Flat array
new Glide(el, { options: [{ value: 1, label: 'One' }] });

// Grouped array
new Glide(el, {
  options: [
    { label: 'Fruit', options: [{ value: 'a', label: 'Apple' }] },
    { value: 'z', label: 'Zucchini' },
  ],
});

// Native <select> / <optgroup> — parsed automatically, no `options` needed
new Glide('#el');

// Async / remote
new Glide(el, {
  async load(query, { page, signal }) {
    const res = await fetch(`/api/search?q=${query}&page=${page}`, { signal });
    const { items, hasMore } = await res.json();
    return { items, hasMore };
  },
});

Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | multiple | boolean | from <select multiple> or false | Multi-select mode | | disabled | boolean | false | Initial disabled state | | searchable | boolean | true | Show the search input (ignored — always on — when load is set) | | fuzzy | boolean | false | Enable subsequence fuzzy matching once exact/startsWith/contains all miss | | theme | 'light' \| 'dark' \| 'auto' | 'light' | 'auto' follows prefers-color-scheme; dark never applies unless requested | | presentation | 'dropdown' \| 'chips' \| 'always-open' \| 'accordion' \| 'popup' | 'dropdown' | Choose classic overlay, wrapping option chips, persistent list, an in-flow accordion, or a wide responsive popup grid | | showSelectedChips | boolean | true | Show selected multi-select values as removable chips in the control. Set false for a cleaner always-open list; a searchable input remains available when enabled | | staticControlLabel | string | — | Keep the control text fixed instead of replacing it with selected values; useful for popup filter triggers and accordion headings | | panelLabel | string | — | Optional panel heading; especially useful for popup filters | | className | string | — | Extra class(es) on the root element, for scoping instance-specific CSS | | controlClassName | string | — | Extra class(es) on the control element — an official hook that survives internal re-renders | | dropdownClassName | string | — | Extra class(es) on the dropdown panel. The panel is portaled to <body> by default, so instance-scoped CSS can only reach it through this | | portal | boolean \| Element | true | Panel container: true portals to <body> (escapes overflow/stacking contexts); false keeps it inside the root (scoped CSS, focus traps, shadow DOM); an Element portals it there | | placeholder | string | derived from an empty leading <option>, else '' | Placeholder text | | options | Array | — | Flat or grouped data (ignored for a native <select>) | | value | value | value[] | — | Initial selection | | load | (query, meta) => Promise | — | Async data source. meta is { page, append, signal }. Return an array, or { items, hasMore } for pagination | | debounceMs | number | 200 | Debounce for load() on keystrokes | | loadMoreThreshold | number | 80 | Pixels from the bottom of the list that triggers the next page | | loadOnInit | boolean | true | Call load('') once on mount | | plugins | GlidePlugin[] | [] | See Plugins | | templates | Partial<Templates> | — | Override option, group, tag, value, noResults, loading render functions |

API

select.open();
select.close();
select.toggle();
select.enable();
select.disable();
select.destroy();

select.clear();
select.select(value);
select.deselect(value);
select.setValue(value);      // single value, or an array in multiple mode
select.getValue();

select.setOptions(data);
select.addOption({ value, label });
select.removeOption(value);
select.getOptions();         // normalized items currently loaded
select.getGroups();
select.refresh();            // force a re-render (e.g. after mutating templates)

select.focus();
select.blur();

const unsubscribe = select.on('change', ({ value }) => { /* ... */ });
select.off('change', handler);

Events

open, close, change, search, clear, create, select, deselect, error, destroy.

Custom rendering

Templates return an HTML string and are inserted via innerHTML — labels are escaped by default (escapeHtml under the hood), so plain data can never inject markup. Opting into rich HTML (icons, avatars, badges) is explicit: you build the string yourself in the template function, and you're responsible for escaping any untrusted fields you interpolate.

new Glide('#assignee', {
  options: people,
  templates: {
    option(item, { ranges }) {
      return `<img class="avatar" src="${item.data.avatarUrl}"> ${item.label}`;
    },
  },
});

Keyboard support

Arrow Up/Down, Home, End, Page Up/Down, Enter, Escape, Tab, Backspace (removes the last tag in multi-select when the input is empty), Delete. Typing filters the list when searchable is on, or jumps to the first matching option (native-<select>-style typeahead) when it's off.

Theming

Every visual value is a CSS variable on .glide. Override them globally or scope an override to one instance:

:root {
  --glide-radius: 4px;
  --glide-border-focus: #7c3aed;
  --glide-active: #ede9fe;
  --glide-active-color: #6d28d9;
}

Every instance defaults to theme: 'light' regardless of OS preference. Opt in per instance:

new Glide('#el', { theme: 'dark' });   // always dark
new Glide('#el', { theme: 'auto' });   // follows prefers-color-scheme
select.setTheme('dark');               // change it later

Full variable list: --glide-bg, --glide-border, --glide-border-focus, --glide-radius, --glide-color, --glide-placeholder, --glide-hover, --glide-active, --glide-active-color, --glide-shadow, --glide-tag-bg, --glide-tag-color, --glide-highlight-bg, --glide-highlight-color, --glide-group-color, --glide-control-min-height, --glide-font-size, --glide-scrollbar-thumb, --glide-scrollbar-track (WebKit + scrollbar-color, both driven by the same pair), and --glide-panel-max-h (caps the option list's height below the default 280px; the available-viewport clamp still applies).

Presentation modes

The same data, selection API, keyboard behavior, plugins, and theme variables work across all presentations:

new Glide('#categories', {
  multiple: true,
  presentation: 'chips',
});

new Glide('#facilities', {
  multiple: true,
  presentation: 'always-open',
  showSelectedChips: false,
  plugins: [checkboxSelection()],
});

new Glide('#region', { presentation: 'accordion' });

new Glide('#property-type', {
  presentation: 'popup',
  panelLabel: 'Property type',
});

Chips and popup use a responsive static option grid; classic dropdown, always-open, and accordion retain virtual scrolling. Useful layout variables include --glide-option-gap, --glide-option-radius, --glide-chip-bg, --glide-chip-selected-bg, --glide-panel-max-h, --glide-popup-width, --glide-chip-selected-color, --glide-chip-radius, --glide-popup-columns, and --glide-popup-padding.

Accordion selection intentionally leaves the panel open so users can continue browsing or make additional choices; it closes only when explicitly toggled, with Escape, or by clicking outside.

Animations are CSS-only (a subtle fade/scale on open) and respect prefers-reduced-motion.

Avoiding a flash of the native select

Like any JS-driven progressive enhancement, there's a brief window between first paint and your script running where the native <select> is visible as-is. Glide hides it once it initializes, but can't do anything before that. If it's noticeable, hide it yourself up front and only reveal Glide's markup:

select.glide-pending { visibility: hidden; }
<select id="country" class="glide-pending">...</select>

Glide's own markup (.glide) isn't hidden by this, since it's a sibling element inserted only once JS runs — nothing extra to unhide.

Sizing

The root element is a block box, so — like most form controls — it stretches to fill its container by default. Constrain it with plain CSS, either against the className option or the fact that the root is always mounted immediately after the original element:

new Glide('#country', { className: 'country-select' });
.country-select { max-width: 320px; }
/* or, without className: */
#country + .glide { max-width: 320px; }

There's also a --glide-max-width variable (default none) if you'd rather set it globally for every instance on the page.

Plugins

Core stays deliberately small; everything below is opt-in:

import { Glide } from '@cliqthemes/glide-dropdown';
import { clearButton } from '@cliqthemes/glide-dropdown/plugins/clearButton';
import { checkboxSelection } from '@cliqthemes/glide-dropdown/plugins/checkboxSelection';
import { selectAll } from '@cliqthemes/glide-dropdown/plugins/selectAll';
import { groupSelect } from '@cliqthemes/glide-dropdown/plugins/groupSelect';
import { tagCreate } from '@cliqthemes/glide-dropdown/plugins/tagCreate';

new Glide('#el', {
  multiple: true,
  plugins: [clearButton(), checkboxSelection(), selectAll(), groupSelect(), tagCreate()],
});

Using the UMD build instead? Every plugin above is already attached to the global — Glide.plugins.clearButton() and so on, no separate import.

| Plugin | Adds | | --- | --- | | clearButton() | An "×" button that clears the selection, hidden when empty | | checkboxSelection() | Checkbox indicators in front of every option | | selectAll() | A "Select all / Clear all" button above the list (multi-select) | | groupSelect() | A "Select all" affordance per group header (multi-select) | | tagCreate({ validate, format }) | Press Enter on unmatched text to create a new option |

Virtual scrolling and group-collapse are core behavior, not plugins — they're fundamental to the performance and Groups requirements, not optional add-ons. Animations are pure CSS, so there's no JS plugin for them either.

Writing a plugin

A plugin is a factory returning { name, init(instance), destroy(instance) }. instance exposes the full public API above, plus instance.dom (root, control, input, dropdown, viewport, content), instance.templates (mutable — wrap a template function to change rendering), and instance.getOptions() / instance.selectedSet.

export function myPlugin() {
  return {
    name: 'myPlugin',
    init(instance) {
      instance.on('change', () => console.log(instance.getValue()));
    },
    destroy(instance) {
      // undo whatever init() did
    },
  };
}

Performance

The option list is virtualized (src/virtual/VirtualList.js): regardless of whether you load 10 or 20,000 options, only the rows intersecting the viewport (plus a small overscan) exist in the DOM, and elements are recycled between scroll frames rather than recreated. Search matching is a single linear pass per keystroke with early-exit ranking (startsWith beats contains beats fuzzy), and store updates are batched into a microtask so a burst of synchronous state changes only triggers one render.

Browser support

Modern evergreen browsers (Chrome, Firefox, Safari, Edge). No IE support.

Project layout

src/
  core/       Glide class, data normalization, async loader, row computation
  store/      Small reactive state container
  renderer/   Control, list, and dropdown-positioning DOM rendering
  keyboard/   Key-to-intent mapping
  search/     Matching (startsWith/contains/fuzzy) + highlighting
  virtual/    Windowed list renderer
  plugins/    Built-in opt-in plugins
  utils/      DOM helpers, escaping, debounce, event emitter
  styles/     Base structural CSS + light/dark themes

License

MIT