Designer view

Select

A control that lets the user choose one or more values from a fixed list. Distinct from Combobox by the absence of a text-input front (no typeahead filtering); the user clicks or keyboard- navigates to a value and commits. Distinct from Radio by presenting choices in a popup rather than as visible alternatives. Renders as native `<select>` by default; custom implementations follow the APG Listbox pattern with portal- mounted popup, `aria-activedescendant` highlighting, and typeahead by first-letter.

When to use

Use

For choosing one or more values from a fixed list of ~3–25 options where typeahead-by-first-letter is sufficient. Common cases: country picker (when the list is short or grouped), priority picker, status setter, single-step config dropdowns. Native `<select>` is the canonical fallback and the right choice for mobile-first or platform-styled forms.

Avoid

For long lists where filter-by-typing improves discovery — that is `Combobox`. For 2–3 mutually-exclusive choices where all options should be visible — that is `Radio`. For multi-select where selections appear inline as removable tokens — that is `TagInput`. For free-text input with autocomplete suggestions — that is `Combobox`.

Versus related

  • combobox

    `Combobox` adds a text-input front for typeahead filtering; `Select` requires the user to scan or first-letter-jump through the visible list. Migrate from Select to Combobox when the option count crosses the "scrollable popup is annoying" threshold (~25 items in practice) or when the user benefits from partial-match filtering.

  • radio

    `Radio` shows all options visible side-by-side or stacked, mutually-exclusive; `Select` hides options until the trigger is activated. Use Radio for 2–4 options where the choice itself communicates the decision (gender, plan tier, payment method). Use Select for 5+ options or when screen real estate is constrained.

  • tag-input

    `TagInput` accepts multi-select with selected values rendered inline as removable tokens; `Select[multi]` shows a count or list in the trigger. TagInput is preferred for visible-on-page selection state where the user benefits from seeing tokens; Select is preferred for compact triggers in dense forms.

Highlight
Fig 1.1 · Select · Designer view
Designer

Figma anatomy

Slot Figma type Hint
trigger instance Button-like surface with value display and caret; min-width drives field size
value-display text Inline text style; truncates with ellipsis when overflowing
caret instance Icon component instance; rotation bound to expanded state
listbox frame Floating frame; width matches trigger by default
option instance Option component instance with selected and highlighted variants
option-group frame Group label plus indented options; visibility bound to "has groups" property
Designer

Token usage per slot

trigger
spacing
  • paddingspacing.compact
  • gapspacing.compact
radius
  • cornerradius.md
color
  • backgroundcolor.surface.bg
  • foregroundcolor.text.primary
  • bordercolor.border.strong
  • ringcolor.border.focus
typography
  • sizetext.md
value-display
color
  • foregroundcolor.text.primary
typography
  • sizetext.md
  • lineHeightleading.snug
caret
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
color
  • foregroundcolor.text.primary
typography
  • sizetext.md
Both

Figma ↔ Code property map

FigmaTypeCodeNotes
VariantVariantvariantMaps default / inline.
SizeVariantsizesm / md / lg.
MultiBooleanmultiToggles single-vs-multi-select. Multi-select trigger displays a count by canon; comma-separated list is anti-pattern.
RequiredBooleanrequiredDrives HTML5 validation on submit; canonical inline-error pattern via `aria-invalid` plus `aria-describedby`.
NativeBooleannativeToggles native `<select>` rendering vs custom listbox. Canonical default is custom; native is an opt-in for mobile-first or platform-styled forms. At and below `breakpoint.sm` native becomes the canonical default regardless of authored value.
Has GroupsBooleangroupsToggles the option-group slot. Use for long lists categorised by domain (countries by continent, files by folder).
DisabledBooleandisabledDisables the entire select. Disabled options are a separate concern (per-option Boolean).
PlaceholderTextplaceholderShown in value-display when nothing is selected. SR users hear it as the trigger's accessible name fallback.
CaretInstance SwapcaretSwap the chevron glyph; defaults vary per design system (down-arrow, triangle, double-arrow).
Designer

Motion

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

Responsive behaviour

BreakpointChange
breakpoint.smAt and below, the canonical default switches to `native: true` — the platform picker provides better UX on touch (full-screen native picker, scroll-wheel on iOS, native search on Android). Visual variants (size, density) still apply to the trigger; the popup rendering follows the platform.
breakpoint.mdAbove this width, custom listbox renders as authored. Visual fidelity matches the design system; keyboard contract follows APG.
Both

Internationalisation

RTL · mirroring

Caret moves from inline-end of the trigger (visual right in LTR) to inline-end (visual left in RTL) via logical positioning. Listbox alignment follows the trigger's inline-start edge in both directions. Caret rotation is direction-neutral (down/up arrows are symmetric). Option content inherits document direction; mixed-direction labels (Hebrew options in an English-default form) honour each option's own `dir` attribute.

Text expansion

Trigger value display truncates with ellipsis when option labels exceed the trigger's inline-size; SR users hear the full label via `aria-label` on the trigger or `aria-activedescendant` on the option. Listbox width matches trigger width by default; long option labels may need a max-inline-size override. Long-text languages (German, Russian) frequently exceed common trigger widths — design size sm with care, or default to size md for international form designs.

Both

Variants, properties, states

Variants

Structurally different versions of the component.

defaultinline

Properties

The same component, parameterised.

PropertyType
multiboolean
requiredboolean
sizesm | md | lg
nativeboolean

States

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

KindStates
interactive
hoverfocus-visibleactivedisabled
data
closedopeninvalid
Both

State transitions

FromToTrigger
closedopenUser clicks the trigger, presses Enter / Space / ArrowDown / ArrowUp on the focused trigger, or types a printable character (typeahead opens and selects first matching option). `aria-expanded` flips to true; listbox renders.
openclosedUser selects an option (single-select); presses Escape; clicks outside the listbox; or tabs focus past the trigger. `aria-expanded` flips to false; listbox is removed.
closedinvalid`required: true` and the user blurs the trigger without making a selection (or commits a form containing an unselected required select). The trigger surfaces an inline error and sets `aria-invalid="true"`.
invalidclosedUser makes a selection (regardless of which option). `aria-invalid` is removed; the inline error is cleared.
Both

Figma↔Code mismatches

  1. 01
    Figma

    A select drawn as a custom popup with the native chevron icon

    Code

    A custom listbox (portal-mounted div with role="listbox") with full APG keyboard contract

    Consequence

    Designers may draw a custom select with the native browser chevron; developers face a choice between native `<select>` (loses visual control over options, follows OS picker) and a full custom listbox (gains visual control, requires implementing the entire APG keyboard contract: typeahead, ArrowKeys, Home/End, PageUp/PageDown, `aria-activedescendant`, multi-select chord-keys). The Figma file frequently does not encode which path was chosen.

    Correct

    Document `native: boolean` as a first-class property. The canonical default is custom (matches design-system visuals); native is an opt-in for situations where the platform picker is preferred (mobile, system-styled forms). The Figma file carries `native` as a Boolean.

  2. 02
    Figma

    Multi-select drawn with comma-separated value list growing in the trigger

    Code

    Multi-select with selected-count display ("3 of 5 selected") that does not reflow the trigger

    Consequence

    Designers compose multi-select mocks with selected values listed inline in the trigger ("Apple, Banana, Cherry, Durian, Eggplant"); the trigger grows unboundedly and truncates at the inline-end. Developers ship the mock faithfully and selecting many items breaks the form layout.

    Correct

    Multi-select trigger displays a count ("3 selected" or "3 of 5") rather than a comma-separated list. The visible value is intrinsically bounded; long selections do not break layout. Document the count-display pattern and reserve comma-separated for very-short maximum selections.

  3. 03
    Figma

    Select drawn at native `<select>` styling with no custom variant

    Code

    A custom listbox replicating native `<select>` to within a few pixels

    Consequence

    Designers may approve a "use native" path then designers rebuild the option list visually because the native picker ignores most CSS. Developers shipping native lose visual parity with the design; shipping custom required reimplementing the platform keyboard contract.

    Correct

    Document the trade-off canonically. Native is invisible to CSS for the popup itself (only the trigger is style-able); custom requires implementing the entire APG contract. Choose per use case; document via `native` property; ship one or the other consistently per design system.

  4. 04
    Figma

    Disabled options drawn greyed-out with no `aria-disabled`

    Code

    Disabled options that are not focusable, not announceable, and silently un-clickable

    Consequence

    Designers grey out unavailable options visually; developers either skip them in the listbox (SR users do not know they exist) or include them with no `aria-disabled` (SR announces them as available, click is a silent no-op).

    Correct

    Disabled options are present in the listbox with `aria-disabled="true"`. SR users hear "<label>, dimmed, option" — they know the option exists and is unavailable. Selection skips disabled options on arrow-key navigation; direct click is a no-op (no error toast, just silent skip).

Designer

Common mistakes

#select-not-button-or-listbox

Select implemented as a styled `<div>` with click handlers

Problem

The trigger is a `<div>` with `onclick` opening a popup that has no `role="listbox"`. SR users hear nothing about a select — no role, no expanded state, no controls reference. Keyboard users get whatever the consumer wired manually (often nothing).

Fix

Trigger is a `<button>` (or native `<select>`); listbox has `role="listbox"`; options have `role="option"`. Mature primitives (Radix Select, React Aria `useListBox`+`useSelect`) provide the entire contract.

#select-no-typeahead

Typeahead by first-letter not implemented

Problem

User types a letter expecting to jump to the next option starting with that letter (a canonical APG behaviour since OS-level select pickers); nothing happens or the letter is captured by an unrelated handler. Users with long option lists must arrow-key through every entry.

Fix

Implement first-letter typeahead per APG: typing matches the first option whose label starts with that letter, moving `aria-activedescendant`. Sequential same-letter presses cycle through matches. Multi-character timeout (~500ms) accumulates the buffer to allow "fer" jumping to "Ferrari" before "Fiat".

#select-listbox-clipped

Listbox clipped by parent `overflow: hidden`

Problem

The listbox is rendered as a child of the trigger's DOM ancestor. A scrollable parent clips the listbox when it extends beyond. Z-index does not help because the clipping is at paint time. Same failure mode as `popover-z-index-clipping`.

Fix

Render the listbox via a portal at the document root. Native `<select>` portal-mounts automatically. Custom implementations use a Portal / Teleport primitive.

#select-required-silent-failure

Required select submits without selection silently

Problem

`required: true` on a select; user submits the form without selecting; submission proceeds (custom form handler) without error indication, or the form rejects with a generic "fill all fields" message that does not identify the specific select.

Fix

Trigger HTML5 validation on submit (native `<select>` does this automatically; custom uses `setCustomValidity()` plus `aria-invalid`). On validation failure, focus the unselected select and surface an inline error referenced via `aria-describedby`.

Accessibility hints
Slot Accessibility hint
trigger Apply `role="combobox"` (the APG Combobox pattern covers select-without-textbox via `aria-haspopup="listbox"` plus `aria-activedescendant`), `aria-expanded` reflecting open state, `aria-controls` referencing the listbox id. Native `<select>` is the canonical fallback — has all these contracts implicitly via the platform.
value-display Plain text. The trigger's accessible name is the value display when the select has no separate label; pair the trigger with a visible `<label>` for complete a11y. Multi-select counts ("3 selected") work canonically; comma-separated lists may overwhelm SR users on long selections.
caret Decorative — `aria-hidden="true"`. Open state is communicated by `aria-expanded`; the caret visualises it.
listbox Apply `role="listbox"` and an `id` referenced by the trigger's `aria-controls`. Listbox is rendered via portal but DOM focus stays on the trigger; do not move focus into the listbox. `aria-multiselectable="true"` for multi-select variants.
option Apply `role="option"` plus a unique `id` per option. The currently-highlighted option is referenced by the trigger's `aria-activedescendant` (NOT focused — DOM focus stays on the trigger). Selected options have `aria-selected="true"`; multi-select shows a visual check (typically a checkmark).
option-group `role="group"` with `aria-labelledby` referencing the group label. Group label is non-selectable (`role="presentation"` or styled `<span>`). SR announces the group label before announcing options inside.