Designer view

Combobox

A text input combined with a popup list of suggestions. The user types to filter, navigates with arrow keys, and selects an option to commit a value. Distinct from a Select (which has no text input) and from Autocomplete (which is a behavior, not a component name in most design systems).

When to use

Use

When the user selects a value from a finite or near-finite set that benefits from typeahead filtering. The text-input front differentiates it from `Select`: users can type to narrow before committing. Best for option counts of ~10–10,000 with optional async filter.

Avoid

For free-text input with no constrained set of values — that is `Input` or `SearchInput`. For a fixed short list (≤7) where typeahead adds no value — `Select`. For multi-tag input where the set is open-ended and tags are user-created — `TagInput`.

Versus related

  • select

    `Select` has no text-input front; the user picks from a fixed list via click or keyboard. `Combobox` adds a typeahead input plus async and strict modes. Migrate from Select to Combobox when the option count crosses the "scrollable popup is annoying" threshold (~10 items in practice).

  • search-input

    `SearchInput` is free text submitted to a search engine; results appear separately. `Combobox` constrains output to a known set of options and commits on selection. They look similar; the distinguisher is whether the popup is a completion list (Combobox) or a results preview (SearchInput).

  • tag-input

    `TagInput` accepts user-created tags or selections from a set and renders accumulated tags inline. `Combobox` with `variant: multi-select` is the constrained-set version; `TagInput` is the open-set version where users can create new tags.

Highlight
Fig 1.1 · Combobox · Designer view
Designer

Figma anatomy

Slot Figma type Hint
input instance Text input component instance; min-width drives the field size
clear-button instance Icon button instance; visibility bound to "has value" property
trigger-button instance Icon button instance; chevron rotates with `aria-expanded`
listbox frame Floating frame (overlay layer); width matches input by default
option instance Option component instance with selected and highlighted variants
empty-state frame Centered text with optional icon; visibility bound to "no matches" state
Designer

Token usage per slot

input
spacing
  • paddingspacing.compact
radius
  • cornerradius.md
color
  • backgroundcolor.surface.bg
  • foregroundcolor.text.primary
  • bordercolor.border.strong
  • ringcolor.border.focus
typography
  • sizetext.md
clear-button
spacing
  • paddingspacing.tight
radius
  • cornerradius.sm
color
  • foregroundcolor.text.muted
trigger-button
spacing
  • paddingspacing.tight
radius
  • cornerradius.sm
color
  • foregroundcolor.text.muted
listbox
spacing
  • paddingspacing.tight
radius
  • cornerradius.md
color
  • backgroundcolor.surface.raised
  • bordercolor.border.subtle
elevation
  • shadowelevation.lg
option
spacing
  • paddingspacing.compact
radius
  • cornerradius.sm
color
  • foregroundcolor.text.primary
typography
  • sizetext.md
empty-state
spacing
  • paddingspacing.comfortable
color
  • foregroundcolor.text.muted
typography
  • sizetext.sm
Both

Figma ↔ Code property map

FigmaTypeCodeNotes
VariantVariantvariantMaps single-select / multi-select / creatable.
Filter ModeVariantfilterModestartsWith / contains / fuzzy / none. Figma encodes for documentation; code drives the filter function selection.
StateVariantdata-stateFigma exposes closed / open / busy / invalid as a Variant for preview. In code these are data states toggled by application logic, mirrored on the host as a `data-state` attribute.
Has Clear ButtonBooleanclearSlot-visibility toggle; code conditionally renders the clear-button slot when the input has a value.
Has Trigger ButtonBooleantriggerOptional disclosure affordance; code conditionally renders the chevron trigger.
StrictBooleanstrict
AsyncBooleanasyncIn Figma toggles the busy-state preview. In code marks the filter as async (results arrive via a Promise / observable).
VirtualisedBooleanvirtualisedDocumentation-only in Figma; in code drives the listbox-rendering strategy above ~200 options.
PlaceholderTextplaceholder
Designer

Motion

TransitionDuration token
openmotion.duration.fast
closemotion.duration.fast
filtermotion.duration.instant
Easing
motion.easing.decelerate
Reduced motion
Instant (jump cut)
Designer

Responsive behaviour

BreakpointChange
breakpoint.smAt and below, the floating listbox is replaced with the platform bottom-sheet picker (`<select>` on iOS, drawer-style native list on Android). The trigger button opens the OS-level surface; the in-page listbox markup is suppressed and `aria-controls` no longer applies. Filtering becomes synchronous (no async results) and the chip-input multi-select degrades to comma-separated tokens in the input.
breakpoint.mdAbove this width, the floating listbox renders as authored — `aria-activedescendant` model, `aria-controls` reference, all framework-map mechanisms apply. Multi-select chips wrap inline with the input.
Both

Internationalisation

RTL · mirroring

Clear-button and trigger-button move from inline-end of the input to inline-start (visual right→left flip) via logical positioning. Chevron rotation is *not* mirrored — the rotation indicates open/closed state, not direction. Listbox aligns to the input's inline-start edge (same logical anchor in both directions). Input caret follows document direction; mixed-direction input (typing Hebrew into a Latin-default field) honours the input's own `dir` attribute.

Text expansion

Option labels can grow significantly; listbox width matches the input by default and may need a max-width override when option labels exceed the input width. Truncation with ellipsis is the canonical fallback for option labels exceeding the listbox width; aria-label on the option provides the full text for SR users. Placeholder text is the riskiest expansion target — many locales' "Search…" translations exceed 30% length.

Both

Variants, properties, states

Variants

Structurally different versions of the component.

single-selectmulti-selectcreatable

Properties

The same component, parameterised.

PropertyType
filterModestartsWith | contains | fuzzy | none
strictboolean
virtualisedboolean
asyncboolean

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibleactivedisabled
data
closedopenbusyinvalid
Both

State transitions

FromToTrigger
closedopenUser types a printable character into the input, presses Down/Up arrow with the popup closed, or clicks the trigger button. `aria-expanded` flips to `true`; the listbox renders.
openclosedUser presses Escape with the popup open, clicks outside the combobox, or selects an option (single-select). `aria-expanded` flips to `false`; the listbox is removed.
openbusyTyped input triggers an async filter request (`async: true`). Existing options are replaced with a loading affordance; an `aria-live` announcement signals the in-flight state.
busyopenAsync filter results return successfully. The option list re-renders with the new matches; an `aria-live` announcement signals the result count.
openinvalid`strict: true` and the input blurs with a value that does not match any option. The combobox surfaces an inline error and sets `aria-invalid="true"` on the input.
invalidclosedUser clears the input or selects a valid option from a subsequently-opened listbox. `aria-invalid` is removed; the inline error is cleared.
Both

Figma↔Code mismatches

  1. 01
    Figma

    A combobox drawn as a text input with a dropdown panel rendered beneath, both inside the same Figma frame

    Code

    The listbox is rendered in a portal at the end of the document body, positioned with floating-ui or popper, and z-indexed above other content

    Consequence

    Designers approximate the listbox at "natural" stacking order, then are surprised when the implementation appears to "escape" its container (e.g., over a parent modal or sticky header).

    Correct

    Document the portal model in the canonical reference and the Figma file. Designers keep the dropdown rendered for review purposes but treat it as a floating layer; developers always portal in production.

  2. 02
    Figma

    Each typeahead state (default / focused / open / loading / invalid / disabled) modeled as a Figma variant

    Code

    Data states (`open`, `busy`, `invalid`) toggled by the application; interactive states (`focus-visible`, `disabled`) are CSS pseudo-classes

    Consequence

    Variant explosion (6 states × 3 sizes × 2 widths = 36+ variants) and ambiguity about which states are mutually exclusive.

    Correct

    Reserve Figma variants for the structural variants (single / multi / creatable). States are documented once in the canonical reference, with a clear matrix of which are mutually exclusive.

  3. 03
    Figma

    Multi-select rendered as chips inside the input field, drawn statically

    Code

    Chips are rendered dynamically; their addition / removal triggers re-layout that shifts the typing caret position

    Consequence

    The dynamic re-layout is invisible to the static Figma file; developers ship comboboxes whose typing behaviour breaks when chips wrap to a new row.

    Correct

    Document the multi-select chip behavior explicitly: chips wrap naturally, the input width is the remaining row space (with a minimum), and adding / removing a chip preserves the caret position. Annotate the Figma frame with a note about the dynamic re-layout.

  4. 04
    Figma

    A "loading" spinner drawn inside the listbox as a permanent visual element

    Code

    The busy state replaces (not supplements) the option list while async results are in-flight, and is announced via a live region

    Consequence

    The spinner appears on every render in mocks, encouraging an implementation that always shows a spinner; assistive-tech users get no announcement when results arrive.

    Correct

    Treat busy as a data state replacing the option list. Document that an `aria-live` announcement fires when results change. The Figma file uses a state-toggle to show busy vs. loaded explicitly.

Designer

Common mistakes

#combobox-focus-into-listbox

Arrow Down moves DOM focus into the listbox

Problem

The implementation uses real `tabindex` on each option and moves focus there. Typing no longer routes to the input; Escape behaves unexpectedly.

Fix

Keep DOM focus on the input. Track the highlighted option with `aria-activedescendant` referencing the option's `id`. Arrow keys mutate the highlighted index; Enter selects.

#combobox-no-aria-expanded

`aria-expanded` not toggled on open / close

Problem

The input misses `aria-expanded`, so screen readers cannot tell whether the popup is open. Users hear "combobox" with no state cue.

Fix

Set `aria-expanded="true"` when the listbox is open, `false` when closed. Keep this in sync with the `data-state` / transition state of the popup.

#combobox-strict-without-feedback

Strict mode silently rejects invalid input on blur

Problem

The combobox is strict (must select from the list) but on blur with an unmatched value the input simply reverts. The user sees their typing disappear with no indication why.

Fix

On blur with an unmatched value, either snap to the best match (with an `aria-live` announcement) or surface an `invalid` state with an inline error referenced by `aria-describedby`. Never silently revert.

#combobox-non-virtualised-large-list

A list of 5,000 options renders all DOM nodes

Problem

Every option is in the DOM whether visible or not. Open latency, memory, and screen-reader announcement all suffer.

Fix

Virtualise the option list when it exceeds ~200 items. Keep the visible window plus a small overscan in the DOM; emit a live-region count ("100 results") so users know the size without scrolling.

#combobox-clear-button-tab-stop

Clear button appears in the keyboard tab order

Problem

Tabbing from the input lands on the × button instead of moving past the combobox. The natural keyboard flow expects the clear affordance to be operable via the input itself.

Fix

Give the clear button `tabindex="-1"` and bind Escape on the input (when the popup is closed) to clear the value. The mouse user can still click the button; the keyboard user uses Escape.

Accessibility hints
Slot Accessibility hint
input `role="combobox"`, `aria-expanded` reflecting popup state, `aria-controls` referencing the listbox id, and `aria-activedescendant` pointing at the currently-highlighted option when navigating with arrow keys. The input retains DOM focus throughout — focus never moves into the listbox.
clear-button Real button with accessible name ("Clear" or "Clear search"). Clicking returns focus to the input. Escape with the popup closed performs the same action (clear value), per APG.
trigger-button `tabindex="-1"` because the input owns focus; `aria-label` ("Show options"). Clicking toggles `aria-expanded` on the combobox input, not on this button.
listbox `role="listbox"` and `id` referenced by the input's `aria-controls`. Listbox is rendered in the document but positioned via portal; do not move DOM focus into it.
option `role="option"` and a unique `id` per option. The currently- highlighted option is referenced by the input's `aria-activedescendant`. Selected options have `aria-selected="true"`.
empty-state Announce the no-results state to assistive tech via `role="status"` (polite) on a live region inside the listbox, or via `aria-live="polite"` on the empty-state container. Avoid putting `role="option"` on the empty-state — it is not selectable.