Select Combobox Dropdown

£5

A custom form select in single and multi flavours: grouped options filter as you type, the highlight is one sliding pill, chosen values become chips, and a real hidden select carries the value into your form.

intermediate
5 more details
micro-interactionstaggerfilterclick-togglehover-effect
Select Combobox Dropdown - UI component preview

This element reads better at your own screen size than in the frame below:

About this component

A drop-in replacement for the browser's select, in the two shapes a product actually needs: a single-select with typeahead filtering over grouped options, and a multi-select whose chosen values sit in the field as chips that arrive and leave under their own animation. The panel measures the room under the field and flips above when there is none, lands a frame before its options stagger in, and moves one highlight pill between rows rather than repainting a background.

Read the full effect overview

It is still a form control. Each field keeps a real <select> in the markup, reads its initial value from it and writes every change back to it, so an ordinary submit sends the values with no extra wiring, and a page whose JavaScript never arrives is left with a working native field. Vanilla JavaScript and core GSAP: no framework, no plugins, no build step.

What's included

9 items
  • Single-select and multi-select from the same script, switched by one attribute
  • Typeahead filtering across labels and their meta line, with empty groups dropping out
  • One highlight pill that slides between rows for arrow keys, hover and click alike
  • Selected values become chips inside the field, animated in and out, with Backspace removing the last
  • The tick travels from the old row to the new one in single-select; multi-select ticks each row it owns
  • Placement measured on open: below by default, flipped above when the room runs out, or pinned either way
  • Writes to a real hidden <select>, reads its initial value from it, and emits a bubbling change event
  • combobox / listbox / option / group roles with aria-activedescendant, and focus that never leaves the field
  • Disabled options, an empty state and a clear button, in light and dark themes

Perfect for

4 use cases
  • Assignee, owner, customer and reviewer pickers in an admin screen
  • Label, tag and category fields where several values are normal
  • Long country, currency and timezone lists that need grouping and search
  • Any form where the native select is the last thing that still looks unstyled

How it works

3 sections

The panel and its options

Opening measures the space under the field against the panel's own height and flips it above when there is not enough, so a field low on the page still shows its whole list. The panel fades and lifts into place, then the visible rows stagger up a moment behind it, so the surface lands before its contents settle rather than the whole thing arriving as one card.

The sliding highlight and the travelling tick

The active row is one pill that glides to whichever row is highlighted, so arrow keys, hover and filtering all move the same object instead of toggling a background colour. Single-select owns one tick that travels to the newly chosen row and settles with a soft overshoot; multi-select gives each row its own, scaled in and out as the value goes on and off.

Chips, filtering and the form value

A pick in multi-select pops a chip into the field with a damped overshoot and leaves the panel open; removing one, by its own button or by Backspace in an empty field, scales it out and takes it with it. Typing filters on the label and its meta line, hides empty groups, and re-staggers whatever survives. Every change writes the selection back to the hidden native select and dispatches a bubbling change event carrying the values and labels.

Difficulty Intermediate
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Paid UI element

Purchase to unlock the code.

Buying Select Combobox Dropdown opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects.

£5 Standard license, unlimited projects

Browse UI elements

Documentation

What's Included

  • index.html: the demo page and the markup to copy

  • assets/style.css: component styles, plain CSS custom properties for colours

  • assets/script.js: readable, commented source with an onReady guard

  • One script drives both shapes: add data-select-multiple and the same field becomes a multi-select with chips

  • Typeahead filtering over each option's label and its meta line, with a group heading dropping out when it empties and an empty state when nothing survives

  • A single sliding highlight pill, tweened to the row's measured offset, shared by arrow keys, hover, click and filtering

  • In single-select, one tick travels from the old row to the new one; in multi-select each chosen row scales its own tick in

  • Chips animate in on a pick and out on removal, by their own button or by Backspace in an empty field

  • Placement measured on open: auto flips above when the room below runs out, below and above pin it

  • Drives a real hidden <select>: it supplies the initial value, receives every change, and submits with the form

  • combobox / listbox / option / group roles with aria-activedescendant, and focus that never leaves the field

  • Disabled options, a clear button, and light and dark themes on one token list

Quick Start

1. Add to your <head>:

Code snippet omitted: it ships with the download.

2. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

3. Copy a .sel block from index.html into your form.

The script finds every element with data-select and wires the hooks inside it:

Attribute On What it is
data-select the field root Marks a field. Required
data-select-multiple the field root Makes it a multi-select with chips
data-select-placement the field root auto (default), below or above
data-select-control the clickable field box Required
data-select-input the <input role="combobox"> Required
data-select-listbox the panel Required
data-select-options the scrolling box inside it Required; the pill and the tick measure against it
data-select-pill the sliding highlight Optional
data-select-mark the travelling tick (single-select) Optional
data-select-chips where chips are appended (multi-select) Required for chips
data-select-clear the clear button Optional
data-select-empty / data-select-empty-term the no-results line and the term inside it Optional

Each option is an element with role="option", data-value (what the form submits), data-label (what is shown and searched) and an optional data-meta (also searched). aria-disabled="true" makes one unselectable and skips it with the arrow keys. Options can sit in role="group" blocks with an aria-labelledby heading, or directly in the listbox.

4. Keep the <select>. It is not decoration:

Code snippet omitted: it ships with the download.

Whatever is selected there becomes the field's initial value, every pick is written back to it, and the form submits from it. .sel-native { display: none } keeps it out of the tab order and the accessibility tree; a display: none control still submits, which is the whole reason the value lives there rather than in a JavaScript variable. Swap it for hidden inputs if you prefer, by changing syncNative().

Listen for changes with the bubbling change CustomEvent on the field root:

Code snippet omitted: it ships with the download.

The variant switcher, the placement buttons and the .stage* blocks in index.html are demo furniture, not part of the component; leave them behind.

Keyboard & Accessibility

Key What it does
ArrowDown / ArrowUp Opens the panel, then moves the highlight, skipping disabled options and wrapping at the ends
Home / End Jumps to the first or last selectable option
Enter Selects the highlighted option. Single-select closes; multi-select stays open for the next one
Escape Closes the panel. Focus stays in the field
Tab / Shift+Tab Closes the panel and moves on to the next control
Any character Filters the options; the highlight moves to the first match
Backspace In a multi-select with an empty field, removes the last chip and its value
  • Roles: the field is an <input role="combobox"> with aria-expanded, aria-controls, aria-haspopup="listbox", aria-autocomplete="list" and aria-activedescendant pointing at the highlighted row. The panel is role="listbox" (aria-multiselectable="true" when it is a multi-select), each row is role="option" with aria-selected, and each block of rows is role="group" labelled by its heading.
  • Focus never leaves the input while the panel is open, which is what the ARIA combobox pattern asks for: there is no focus trap, because there is nothing else to focus. Pointer presses inside the panel are prevented from stealing it, so the field keeps its focus ring throughout.
  • A press outside the field closes the panel without moving focus.
  • prefers-reduced-motion: the same end states with every duration at zero. The panel opens straight into place, the pill and the tick jump rather than travel, and chips appear without a scale. Nothing is disabled.
  • The clear button and each chip's remove button carry their own labels ("Clear assignee", "Remove Blocker"), so they are not two dozen unnamed crosses.

How It Works

Opening and placement. openList() un-hides the panel, then measures: auto compares the panel's own height with the room under the field and adds is-above when there is not enough. GSAP fades and lifts the panel in over 0.22s, and a second tween staggers the visible rows up from 12px behind a small delay, so the surface lands a frame before its contents settle. Closing tweens the panel back and sets hidden in onComplete, because a faded-out panel still covers the field under it and eats every click meant for that one.

The sliding pill and the travelling tick. The highlight is one absolutely positioned element at left: 0; top: 0, moved entirely by x/y/width/height read from the row's offsetLeft/offsetTop. Keeping the CSS offset at zero is deliberate: any padding left in CSS would be added to the measured offset and the pill would sit a step off its row. Measurements are taken against .sel-options and re-run on resize and on document.fonts.ready, so a theme change or a late webfont cannot leave it stranded. Single-select's tick is one element tweened to the newly chosen row with a back.out scale on top; multi-select gives each row its own, since several can be on at once.

Chips and the form value. A pick in a multi-select appends a chip with gsap.from on a back.out ease and leaves the panel open, so picking four labels is four gestures rather than eight. Removing one scales it out and drops the node in onComplete. Every change runs syncNative(), which sets selected on the matching <option> of the hidden <select>, then dispatches the change CustomEvent. That split is the point: the animation layer never holds the value, so what you submit and what you see cannot drift apart.

Customisation

  • Panel height: max-height on .sel-options (default 262px).
  • Row height: min-height on .sel-option (40px, 44px under 560px wide).
  • The highlight's tint is --accent-wash and its ring is --accent; drop the box-shadow on .sel-pill for a flat tint.
  • Timings and easings are the gsap.to / gsap.fromTo calls in assets/script.js; the stagger and the panel's head start are the duration, stagger and delay in openList().
  • Colours live in assets/style.css as custom properties per theme.

Themes

Ships in two themes, light (the default) and dark. They are one design at two token values; set the attribute on the demo's <body>, or on the field root in your own page:

Code snippet omitted: it ships with the download.

Nothing else changes: same markup, same script, and no theme name is ever read in JavaScript. The tokens to re-value for your own brand are --ground, --ground-2, --raised, --hover, --ink, --ink-2, --ink-3, --line, --line-strong, --accent, --accent-ink, --accent-wash, --shadow, --font, --radius, --radius-sm, --radius-xs, plus the component's own --field-bg, --field-border, --panel-bg, --avatar-bg and --chip-bg. To add a third theme, copy either [data-variant="..."] block, rename the value and change the properties; no selector override is needed.

Give --accent-wash a solid hex rather than a color-mix with transparency: it sits behind the highlighted row's text, and a translucent value cannot be contrast-checked against what is underneath it.

Requirements

  • GSAP 3.12+ (core only, no plugins)
  • No build step, no framework

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.

Your cart

Your cart is empty

The Vault £99

Everything in the catalogue, plus everything we release next.