@get-set/gs-select
v1.0.28
Published
Get-Set Select
Maintainers
Readme
GSSelect
A dependency-free, beautifully animated custom <select> / dropdown enhancer available in two flavours from one codebase:
- Native / vanilla JS — a
window.GSSelect(selector, params)factory. - React — a
<GSSelect />component.
Both share the exact same engine (actions/, constants/, helpers/, types/), so behaviour is identical across the two. GSSelect progressively enhances a real native <select> element, so it keeps full form semantics (name, required, validation, FormData) while giving you a fully stylable, animated panel with search, multi-select tags, per-option icons, theming and backdrop layers.
Features
- Modern, token-based control: explicit surface background, hover border, accent focus ring, rotating chevron,
placeholdertext,clearable× button,disabledandloading(spinner) states - Panel open animations:
none,fade,slide,scale,flip— tunableanimationDurationandanimationEasing - Backdrop / overlay catalog rendered behind the open panel:
none,dim,blur,glass,gradient,vignette,tint— fine-tuned viabackdropOptions(opacity,blur,color,colorTo) glassfrosted panel surface (translucent tint + hairline border) andsoftShadowelevated drop shadow- Theming:
auto/light/dark— complete palettes built on namespaced--gss-*design tokens scoped to the container (nothing leaks into the host page, and the host page'sbodyis never repainted) plus a singleaccentCSS color that recolors selection, chips, focus ring and the search underline - Three control + option-row sizes:
sm,md,lg— chips, search and empty-state scale too - Built-in type-to-filter
allowSearchwithsearchPlaceholder(single and multiple mode), customsearchFiltermatcher,onSearchcallback andemptyTextempty state multipleselection with polished chips (always-visible remove ×, touch-friendly),maxSelectionscap and Backspace-to-removedropdownPosition: opendown,up, orauto(flips upward when there is no room below) + azIndexparam for stacking contexts- Custom option HTML via
renderOption, per-option leading icons viadata-icon(emoji, text, or inline SVG) - Selected-option
showCheckmark, custom triggericon,maxHeightscroll cap,closeAfterSelect,rtllayout - Responsive per-breakpoint param overrides — re-evaluated live on window resize
- Lifecycle hooks (
afterInit,afterChange,onOpen,onClose,onSearch) plus the underlying<select>'s nativeonChange - Accessible:
combobox/listbox/optionroles,aria-expanded/aria-selected/aria-activedescendant, labelled chip-remove buttons, full keyboard support (arrows, Home/End, Enter, Escape, Tab-close, type-ahead) - Respects
prefers-reduced-motion: reduce— transitions/transforms disable automatically - Imperative API (open/close/get/set value/refresh/destroy) in both targets
Compatibility
| Target | Requirement |
|---|---|
| React (the <GSSelect> component) | React 16.8+ (Hooks are required), and 17 / 18 / 19. React is an optional peer dependency — you only need it for the component. |
| Native / vanilla (window.GSSelect) | No framework. Any modern evergreen browser. |
| TypeScript | First-class — type declarations (.d.ts) ship in the package. |
| SSR / Next.js | Safe to import server-side (no DOM access at module load). The enhancer is browser-only, so render it inside a Client Component ('use client'). |
The peer range is
^16.8.0 || ^17.0 || ^18.0 || ^19.0, withreact/react-dommarked optional so the native build has zero peer requirements.
Installation
npm i @get-set/gs-selectProject layout
GSSelect.ts # native entry (webpack -> dist-js/bundle.js, window global)
components/GSSelect.tsx # React component (tsc -> dist/, npm entry)
actions/ # shared engine (init, draw, open/close, search, change, refresh, destroy, enhancements)
constants/defaultParams.ts # shared defaults
helpers/uihelpers.ts # shared helpers (GUID, scoped-style injection)
types/ # Params / Ref / Window augmentation
components/styles/ # SCSS + compiled CSS + CSS-as-TS (runtime injection for React)
styles/ # SCSS + compiled CSS (for <link> use by the native build)Build
npm install
npm run build # builds both targets
npm run build:js # native bundle -> dist-js/bundle.js
npm run build:react # React + types -> dist/Tests
Unit tests use Vitest + jsdom:
npm test # run once
npm run test:watchUsage — Native JS
The markup is a plain native <select> (with <option> / <optgroup> children). Add the stylesheet, then enhance the element by selector:
<link rel="stylesheet" href="styles/GSSelect.css" />
<select class="select1" name="country" required>
<option value="" hidden>Select country</option>
<option value="us" data-icon="🇺🇸">United States</option>
<option value="de" data-icon="🇩🇪">Germany</option>
<option value="fr">France</option>
</select>
<script src="dist-js/bundle.js"></script>
<script>
new GSSelect('.select1', {
animation: 'flip',
theme: 'dark',
accent: '#22d3ee',
glass: true,
backdrop: 'blur',
allowSearch: true,
searchPlaceholder: 'Search…',
});
</script>From a CDN:
<script src="https://unpkg.com/@get-set/gs-select/dist-js/bundle.js"></script>
<script>
new GSSelect('.select1');
</script>The native bundle also registers a jQuery plugin and an HTMLElement.prototype adapter, so you can enhance elements directly (every form registers into window.GSSelectConfigue):
// jQuery (when jQuery is present on the page)
$('.select1').GSSelect({ allowSearch: true });
// HTMLElement.prototype
document.querySelector('.select1').GSSelect({ multiple: true });
new GSSelect(...)accepts a CSS selector or a<select>element, so the jQuery / prototype adapters (which pass the element) behave exactly likenew GSSelect('.select1', params).
Instance methods (registry)
Give the instance a reference key, then look it up via the global registry and call methods on the returned ref:
new GSSelect('.select1', { reference: 'country' });
const inst = window.GSSelectConfigue.instance('country');
inst.refresh(); // rebuild the enhanced select (e.g. after options change)
inst.destroy(); // tear down the enhancement + unregisterwindow.GSSelectConfigue holds:
references—Array<{ key: string; ref: Ref }>of every live instance.instance(key)— returns the registeredRefforkey(orundefined).
The returned Ref exposes select (the underlying <select>), currentParams, reference, refresh() and destroy().
Usage — React
import GSSelect from '@get-set/gs-select';
export default function CountryPicker() {
return (
<GSSelect
animation="scale"
theme="auto"
accent="#4f46e5"
allowSearch
searchPlaceholder="Search…"
onChange={(e) => console.log('value', e.target.value)}
>
<option value="" hidden>Select country</option>
<option value="us" data-icon="🇺🇸">United States</option>
<option value="de" data-icon="🇩🇪">Germany</option>
<option value="fr">France</option>
</GSSelect>
);
}The component renders a real <select>; pass <option> / <optgroup> as children. Plugin-only props (see table below) are stripped and never leak onto the DOM <select>; all other standard <select> attributes (name, required, disabled, onChange, …) pass straight through. Styles are injected at runtime — no CSS import required.
Next.js (App Router)
The enhancer is browser-only, so use it from a Client Component:
'use client';
import GSSelect from '@get-set/gs-select';
export default function CountryPicker() {
return (
<GSSelect theme="dark" glass backdrop="blur">
<option value="us">United States</option>
<option value="de">Germany</option>
</GSSelect>
);
}Imperative ref (GSSelectHandle)
The component exposes an imperative handle through its ref (not the raw <select> node):
import GSSelect, { GSSelectHandle } from '@get-set/gs-select';
import { useRef } from 'react';
function App() {
const select = useRef<GSSelectHandle>(null);
return (
<>
<GSSelect ref={select} multiple>
<option value="1">One</option>
<option value="2">Two</option>
</GSSelect>
<button onClick={() => select.current?.open()}>Open</button>
<button onClick={() => select.current?.setValue(['1', '2'])}>Pick both</button>
<button onClick={() => console.log(select.current?.getValue())}>Read value</button>
</>
);
}| Method | Signature | Description |
|---|---|---|
| open() | () => void | Open the dropdown panel. |
| close() | () => void | Close the dropdown panel. |
| getValue() | () => string \| string[] | Current value. Returns a string, or a string[] for multiple selects. |
| setValue(value) | (value: string \| string[]) => void | Set the value (string, or string[] for multiple) and re-sync the UI (also fires the native change event). |
| refresh() | () => void | Rebuild the enhanced select — call after the option set changes. |
| destroy() | () => void | Tear down the enhancement and unregister the instance. |
| getElement() | () => HTMLSelectElement \| null | Escape hatch — the underlying native <select> element. |
Scoped styles with gsx (React only)
The React-only gsx prop injects styles scoped to a single instance (matched by its generated data-key). It accepts a nested CSS object (NestedCSS) where nested keys become descendant selectors:
<GSSelect
gsx={{
// applies to the root <select> for this instance
fontSize: '14px',
// nested selectors target the rendered dropdown parts
'.gs-select-container': { borderRadius: '10px' },
'.gs-select-option': { padding: '8px 12px' },
}}
>
<option value="1">One</option>
<option value="2">Two</option>
</GSSelect>Options
All options work identically in both targets — pass them as React props on <GSSelect> or as the second argument to new GSSelect(selector, params). Params also extends the native SelectHTMLAttributes (minus size), so standard <select> attributes (name, required, disabled, multiple, onChange, className, …) are accepted too.
| Option | Type | Default | Description |
|---|---|---|---|
| reference | string | random GUID | Unique key for the registry / lookups. |
| icon | string | '<icon class="gs-select-default"></icon>' | Trigger icon markup (HTML string, emoji, or text). |
| closeAfterSelect | boolean | true (forced to false for multiple) | Close the panel after picking an option. |
| allowSearch | boolean | true | Show the type-to-filter search box. |
| searchPlaceholder | string | 'Type for searching' | Placeholder text for the search input. |
| maxHeight | number | 300 | Max height (px) of the scrollable option list. |
| disableStyles | boolean | false | Skip injecting the plugin's default styles (bring your own CSS). |
| responsive | Array<{ windowSize, params }> | [] | Per-breakpoint param overrides (applied when window.innerWidth <= windowSize). |
| multiple | boolean | false | Multi-select with rendered tags. Also enabled by the native multiple attribute; forces closeAfterSelect: false. |
| animation | 'none' \| 'fade' \| 'slide' \| 'scale' \| 'flip' | 'scale' | Panel open animation. |
| animationDuration | number (ms) | 220 | Open/close transition duration. |
| animationEasing | string | 'cubic-bezier(0.4, 0, 0.2, 1)' | CSS timing function for the open animation. |
| glass | boolean | false | Frosted glass panel surface (translucent tint + hairline border). |
| softShadow | boolean | false | Softer, elevated drop shadow on the panel. |
| backdrop | 'none' \| 'dim' \| 'blur' \| 'glass' \| 'gradient' \| 'vignette' \| 'tint' | 'none' | Overlay layer rendered behind the open panel. |
| backdropOptions | { type?, opacity?, blur?, color?, colorTo? } | – | Fine-tune the backdrop. type overrides the top-level backdrop string. See below. |
| theme | 'auto' \| 'light' \| 'dark' | 'auto' | Color theme. auto follows prefers-color-scheme. |
| accent | string (CSS color) | #4f46e5 (CSS default) | Accent color for selection, focus ring and the search underline. Sets --gs-accent. |
| rtl | boolean | false | Right-to-left layout (also sets dir="rtl"). |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Control + option-row sizing. |
| showCheckmark | boolean | true | Show a checkmark next to the selected option(s). |
| emptyText | string | 'No results found' | Empty-state message shown when search has no matches. |
| placeholder | string | – | Placeholder shown in the control while nothing is selected (deselects the auto-picked first option on first init; an explicit selected attribute wins). |
| clearable | boolean | false | Show a clear (×) button on the control while a value is selected. Clearing fires the normal change pipeline. |
| disabled | boolean | – | Disable the control (dimmed, not-allowed cursor, opening blocked). Also honoured from the native disabled attribute. |
| loading | boolean | false | Loading state — shows an accent spinner in the control and blocks opening. |
| maxSelections | number | 0 (unlimited) | Cap the number of selected options for multiple selects; extra clicks are ignored until a slot frees up. |
| dropdownPosition | 'down' \| 'up' \| 'auto' | 'down' | Panel opening direction. auto flips upward when there is no room below the control. |
| zIndex | number | 999 (CSS default) | Stacking z-index for the open panel (sets --gs-z; the backdrop sits one level below). |
| searchFilter | (query, label, value) => boolean | contains-match | Custom search matcher. Return true to keep the option visible. |
| renderOption | (option: HTMLOptionElement) => string | – | Custom option renderer — returns an HTML string for the option label. |
| gsx | NestedCSS | – | React only — scoped inline styles for the instance. |
| afterInit | (select: HTMLSelectElement) => void | – | Fired after the enhanced select is initialised. |
| afterChange | (select: HTMLSelectElement) => void | – | Fired after the selection changes (exactly once per change, in both single and multiple mode). |
| onOpen | (select: HTMLSelectElement) => void | – | Fired when the dropdown panel opens. |
| onClose | (select: HTMLSelectElement) => void | – | Fired when the dropdown panel closes. |
| onSearch | (query: string, select) => void | – | Fired on every search keystroke with the current query. |
backdropOptions
| Field | Type | Description |
|---|---|---|
| type | GSSelectBackdrop | Backdrop kind. Overrides the top-level backdrop string when both are set. |
| opacity | number (0–1) | Overlay opacity when the panel is open (clamped to 0–1). Sets --gs-backdrop-opacity. |
| blur | number (px) | Blur radius for blur / glass backdrops. Sets --gs-backdrop-blur. |
| color | string (CSS color) | Base color for dim / tint / glass / gradient / vignette. Sets --gs-backdrop-color. |
| colorTo | string (CSS color) | Secondary color for the gradient / vignette backdrops. Sets --gs-backdrop-color-to. |
Variant catalogs
Animations (animation)
| Value | Description |
|---|---|
| none | No transition — panel appears instantly. |
| fade | Opacity fade in/out. |
| slide | Slide the panel into place. |
| scale | Scale/zoom the panel from the trigger (default). |
| flip | 3D flip reveal. |
Backdrops (backdrop / backdropOptions.type)
| Value | Description |
|---|---|
| none | No backdrop layer (the element is removed). |
| dim | Solid color overlay that fades to opacity while open. |
| blur | Backdrop-filter blur (blur px) behind the panel. |
| glass | Frosted overlay — color tint + blur px blur with extra saturation. |
| gradient | Radial gradient from color to colorTo. |
| vignette | Radial vignette — transparent center (colorTo) darkening to color at the edges. |
| tint | Flat color tint at opacity. |
Themes (theme)
| Value | Description |
|---|---|
| auto | Follows the OS prefers-color-scheme (default). |
| light | Force light theme. |
| dark | Force dark theme. |
Sizes (size)
| Value | Description |
|---|---|
| sm | Compact control + option rows. |
| md | Default sizing. |
| lg | Large control + option rows. |
Per-option leading icons
Add a data-icon attribute to any native <option> (emoji, text, or inline SVG markup) to render a leading icon for that row:
<option value="us" data-icon="🇺🇸">United States</option>
<option value="de" data-icon="🇩🇪">Germany</option>Examples
Native — themed, glass, with backdrop
<link rel="stylesheet" href="styles/GSSelect.css" />
<select class="picker" name="country" required>
<option value="" hidden>Select country</option>
<option value="us" data-icon="🇺🇸">United States</option>
<option value="de" data-icon="🇩🇪">Germany</option>
<option value="fr">France</option>
</select>
<script src="dist-js/bundle.js"></script>
<script>
new GSSelect('.picker', {
reference: 'country',
animation: 'flip',
animationDuration: 260,
theme: 'dark',
accent: '#22d3ee',
size: 'lg',
glass: true,
softShadow: true,
backdrop: 'blur',
backdropOptions: { opacity: 0.4, blur: 8 },
allowSearch: true,
searchPlaceholder: 'Search…',
emptyText: 'Nothing matched',
afterChange: (sel) => console.log('value', sel.value),
});
</script>React — multiple select with tags + ref
'use client';
import GSSelect, { GSSelectHandle } from '@get-set/gs-select';
import { useRef } from 'react';
export default function Tags() {
const ref = useRef<GSSelectHandle>(null);
return (
<>
<GSSelect
ref={ref}
multiple
animation="scale"
theme="auto"
accent="#4f46e5"
showCheckmark
backdrop="gradient"
backdropOptions={{ color: '#1e1b4b', colorTo: 'transparent', opacity: 0.5 }}
onChange={(e) => console.log(Array.from(e.target.selectedOptions).map((o) => o.value))}
>
<option value="1" data-icon="🍎">Apple</option>
<option value="2" data-icon="🍌">Banana</option>
<option value="3" data-icon="🍒">Cherry</option>
</GSSelect>
<button onClick={() => ref.current?.setValue(['1', '3'])}>Pick apple + cherry</button>
</>
);
}Theming & design tokens
All colors flow through namespaced CSS custom properties (--gss-*) scoped to .gs-select-container — the stylesheet never touches :root or the host page's body. Forced theme: 'light' / 'dark' redefine the complete token palette, so every surface (control, panel, chips, search inputs, scrollbars, shadows, glass) is correct regardless of the OS color scheme; theme: 'auto' follows prefers-color-scheme.
Key tokens you can override per instance (via gsx, a wrapper class, or inline style):
.my-scope .gs-select-container {
--gs-accent: #22d3ee; /* also settable via the accent param */
--gss-bg: #ffffff; /* control surface */
--gss-color: #111827; /* text */
--gss-muted: #6b7280; /* secondary text / chevron */
--gss-border: #d1d5db; /* control border */
--gss-panel-bg: #ffffff; /* dropdown panel surface */
--gss-hover: #f3f4f6; /* option hover/highlight */
--gss-chip-bg: #eef2ff; /* multi-select chip */
--gss-radius: 10px; /* control radius */
--gss-panel-radius: 12px; /* panel radius */
}Where supported, color-mix() derives the selection tint, focus ring and chip colors from --gs-accent automatically, so a single accent param re-skins the whole widget in every theme.
Accessibility & motion
- The control is a
role="combobox"witharia-haspopup="listbox",aria-expandedandaria-controlswired to therole="listbox"panel (aria-multiselectableformultiple). - Every option row is a
role="option"with livearia-selected/aria-disabled, and the highlighted row is exposed througharia-activedescendant. - Chip remove buttons carry
aria-label="Remove <label>"; the search input has anaria-labelandaria-autocomplete="list". - Keyboard: ArrowUp/ArrowDown move the highlight, Home/End jump to the first/last option, Enter selects (or opens when closed, along with Space/ArrowDown), Escape closes, Tab closes and moves focus on, Backspace removes the last chip, and typing filters (or type-aheads when
allowSearch: false). - A visible accent focus ring is shown on
:focus-visible(no moreoutline: noneblack holes). - Animations respect
prefers-reduced-motion: reduce— transitions, transforms and the loading spinner animation are disabled automatically.
License
ISC.
