# Select Combobox Dropdown

> 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.

Canonical: https://gsapvault.com/ui-elements/select-combobox-dropdown
Live demo: https://gsapvault.com/demos/select-combobox-dropdown/index.html

| Property | Value |
|----------|-------|
| Type | ui-element |
| Tier | paid |
| Price | £5 |
| Difficulty | intermediate |
| Plugins | Core GSAP only |
| Techniques | micro-interaction, stagger, filter, click-toggle, hover-effect |
| Uses Lenis | No |

## Overview

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.

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.

## Features

- 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

## 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

## Vibe-Code Ready Setup

This UI element includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

### 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.

## Documentation

How this UI element works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### 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

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium UI element. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Select Combobox Dropdown](https://gsapvault.com/ui-elements/select-combobox-dropdown)
- [The Vault (£99 one-time, best value): everything in the catalogue, including future releases](https://gsapvault.com/effects)

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
