Select

User Experience (UX)

How a select behaves for every person, input and setting, from the ARIA pattern and the keyboard to forms, Dialogs and the native picker on touch screens.

Now let's look at how a select behaves for every person, input and setting. A good select is accessible, predictable, designed in every state and adaptive.

Accessible

Use a button with the combobox role

We recommend the APG's select-only combobox, built on a <button> rather than the <div> its example uses. A plain button can already carry aria-expanded, but it has no value of its own for assistive technologies to read, and a form control needs one. The combobox role gives it one: the text inside it. Its label stays its name (WCAG 4.1.2), as the accessibility tree shows.

We use a <button> so the Field can name it. A <div role="combobox"> isn't labelable, so a <label for> can't name it. A <button> is, and ARIA in HTML allows the role on it, so the Field names the Select with the same <label for> it gives an Input (Radix Primitives builds its trigger the same way).

The panel is a listbox of options, with aria-selected on each, and aria-multiselectable when multiple is on. combobox, aria-expanded, aria-controls and aria-activedescendant link the two. Group headings use role="group" with aria-labelledby, and a separator gets role="none", since a listbox doesn't allow one as a child.

Name every select

Every Select needs a name, and the simplest way is to put it in a Field, which gives it a <label for>. Where there's no visible label, like a filter in a toolbar, give it an aria-label (WCAG 3.3.2). Screen readers read the name, then the value, and the panel takes the same name, so the list has a name too. Our reference implementation warns you during development if there's no name.

Let a press on the label focus it, not open it

A press on a <label for> clicks its control, and for a button that would open the panel. A native select only takes focus, so the Select does the same. Keep in mind that detail can't tell the two clicks apart, since a screen reader's activation has a detail of 0 as well, so it's the click on the label itself you'll want to catch.

Keep focus on the control

Focus never moves into the panel. aria-activedescendant on the control says which option is highlighted, and screen readers follow it (WCAG 2.4.3). That's the one place the Select differs from the Popover, which moves focus in: the Popover holds controls people tab between, while typing a letter or pressing an arrow here has to keep working on the control. It also means there's nothing to give focus back to when the panel closes, so it stays where it was.

Announce the value

A combobox reads its value when focus lands on it, so a single value needs nothing more. With several, the tags sit outside the button, so the button holds their labels as visually hidden text, and past three, a count like "4 selected". Removing a value is announced through one polite live region, since a tag vanishing is otherwise silent (WCAG 4.1.3). As people move through the options, aria-selected on each one tells them which are picked.

Contrast and size

Text, borders and the focus ring follow the rules on the UI page (WCAG 1.4.3, WCAG 1.4.11, WCAG 2.4.7). Press targets are at least 24 × 24px (WCAG 2.5.8), and on touch screens at least 44px tall (WCAG 2.5.5).

Predictable

Keyboard

The Select follows the APG's keyboard model, so people who know one native select or one combobox already know this one (WCAG 2.1.1, WCAG 2.1.2). A closed Select:

KeyWhat it does
ArrowDown, Alt + ArrowDownOpens it, without moving the highlight or changing the value
Enter, SpaceOpens it, without moving the highlight or changing the value
ArrowUp, HomeOpens it and highlights the first option
EndOpens it and highlights the last option
A letterOpens it and highlights the option that starts with that letter
BackspaceWith multiple, removes the last value

With the panel open:

KeyWhat it does
ArrowDown, ArrowUpMoves the highlight, skipping disabled options, and stops at the ends
Home, EndMoves to the first or the last option
PageDown, PageUpMoves ten options
A letterMoves to the next option that starts with what people typed
Enter, SpacePicks the highlighted option and closes. With multiple, toggles it and stays open
Alt + ArrowUpPicks the highlighted option and closes. With multiple, closes and keeps what's toggled
TabPicks the highlighted option, closes and moves on. With multiple, closes and keeps what's toggled
EscapeCloses it, without changing the value, and goes no further

Tab taking the highlighted option comes straight from the APG, though native selects differ from one browser to the next, so we follow the one written source. Keep in mind that it means people who open the list and press Tab straight away pick the option that's highlighted, the first one when nothing is picked yet.

Type to find

Typing moves to the option that starts with what people typed, as in the APG's example. Letters typed in quick succession search as one string, spaces included, so "new z" finds New Zealand, and pressing one letter again moves through the options that start with it. Disabled options are skipped. We forget what people typed after 500ms, which we recommend since it's long enough to type a word and short enough to start over.

Remove a value by pointer and by keyboard

Each tag's remove button is a real <button>, named "Remove France", which Tab reaches after the control, since it comes after the control in the DOM (WCAG 1.3.2, WCAG 2.4.3). Backspace on the closed control removes the last value, and focus stays put. When a remove button takes itself away, focus moves to the next remove button, or back to the control when no remove button comes after it, so keyboard users never drop to the top of the page.

Close it every way people expect

Escape, a press outside and picking a single value all close the panel. The panel uses popover="auto", so the browser closes it on a press outside, and it has the Popover's answer to Escape inside other layers: the Select cancels the key, so a Dialog around it stays open.

Render it inside a Dialog

Render the Select inside the Dialog, so the Dialog doesn't make it inert, and the panel goes in the top layer on top of it. Escape closes the panel and stops, so the Dialog stays open, and Escape again closes the Dialog.

Edit address

France
Germany
Spain
New Zealand
Portugal
<Dialog id="address" title="Edit address" open={open} onClose={() => setOpen(false)}>
  <Field label="Country">
    <Select id="country" name="country" options={countries} defaultValue="fr" nativeOnTouch={false} />
  </Field>
</Dialog>

Let the form do its work

A native <select> is always in the page, and it does the form's work. In the custom rendering it's see-through and out of the tab order, and in the native rendering it's the control. So the Select submits under its name, several values included, required works, and a reset goes back to the first value, in either rendering, with no script of your own.

France
France
Germany
Spain
New Zealand
Portugal

<form onSubmit={send}>
  <Field label="Countries" required>
    <Select id="countries" name="countries" multiple options={countries} defaultValue={["fr"]} nativeOnTouch={false} />
  </Field>
  <Button type="submit" color="primary">Send</Button>
  <Button type="reset" variant="outline">Reset</Button>
</form>

Three things make that hold, and each is easy to get wrong:

  • It's never display: none, which makes a required select unfocusable and blocks submit in silence. If the browser focuses it to report a problem, focus moves to the control in that same event handler, since Chromium hides its validation bubble on any later change of focus.
  • A selection in the panel fires a bubbling change from the <select>, so form-level listeners and form libraries hear it.
  • A form's reset goes back to each option's default selectedness, which only the selected attribute sets, so the first value stays written out as that attribute however the framework renders.

Don't pick an option for people

A single Select always starts with a first <option value="" disabled hidden>, holding the placeholder or empty text, selected while there's no value. It's the HTML Standard's placeholder label option, so it counts as missing when required, and it's disabled, so it submits nothing and a native picker can't choose it either.

So with no value and no placeholder, the field starts blank, the value stays unset and nothing is submitted. A bare <select> would pick its first option there, and we depart from it on purpose: people would send an option they never looked at, and your value would disagree with the form. The GOV.UK Design System makes the same call for questions, for fear of swaying the answer. We recommend a placeholder or a first value either way. To let people clear a single value, add an option with an empty value to your options.

Every state

Closed

The field shows the value, or the placeholder at 4.5:1. The panel stays in the page while it's closed, so its options are ready, but it isn't in the accessibility tree.

Open

aria-expanded is "true", the arrow turns and one option is highlighted, the selected one or else the first that isn't disabled. It scrolls into view as the highlight moves, since browsers only do that for elements with real focus.

Disabled

disabled goes on both controls, so it can't be focused and leaves its form, and the Field dims its label. Tags lose their remove buttons. We recommend disabling as little as you can, since a disabled Select can't say why (WCAG 3.3.1).

Read-only

The value stays, in its form, at full contrast with a dashed border. Nothing opens and tags can't be removed, and aria-readonly says so. A native <select> has no read-only state, so readonly turns off nativeOnTouch.

Invalid

aria-invalid and the error's ID on aria-describedby go on the active control, set by the Field, and the border turns to the error colour (WCAG 3.3.1).

Long and empty

A long value is cut off with an ellipsis in the field, and an option's text wraps in the panel, so nothing is lost where people pick. An empty list opens an empty panel, so we recommend not rendering the Select until it has options.

Adaptive

Native on touch

nativeOnTouch shows the native <select> where the primary pointer is coarse, so a phone or a tablet gets its own picker. It's on by default for two reasons: the APG warns that its pattern has support gaps on touch devices, and the native picker is what phone screen reader users already know.

France
Germany
Spain
New Zealand
Portugal
<Field label="Country">
  <Select id="country" name="country" options={countries} placeholder="Choose a country" />
</Field>

"Coarse" is (pointer: coarse), for the primary pointer, rather than a viewport width, since a narrow desktop window still has a mouse. It's not the Input's any-pointer: coarse either: a laptop with a touch screen has a fine primary pointer, so it keeps the custom panel, while the Input's touch sizing applies as soon as any pointer is coarse.

Both controls are in the markup from the start, so the server and the first render agree whatever the device. CSS alone chooses what paints, with a @media (pointer: coarse) rule on the same query, so there's no hydration mismatch and no layout shift. After mount, a script moves the id, the ARIA attributes and data-slot="control" to whichever control is active, along with the tabindex and aria-hidden that only the hidden one has, and it listens for the pointer changing. So the value, the name, the form participation, the validation and the Field wiring are the same in both, as the tests check.

The root's tokens, and the leading and arrow slots, stay in both renderings, since they're children of the field. Here's what the native rendering can't honour, as the browser draws these itself:

  • The value slot and its token, as the browser paints the <select>'s own text.
  • Tags, the tag slot and its tokens.
  • The panel and its tokens.
  • The option, group-label and icon slots and their tokens, and the check mark.
  • The separator slot and its tokens. Browsers draw an <hr> between options in their own way, and older ones leave it out.

If those have to look like your brand on phones too, turn the fallback off with nativeOnTouch set to false, and test with the phones you support.

Pointer, touch and pen

  • Hover styles only apply to pointers that can actually hover (@media (hover: hover)), otherwise a tap would leave the field or an option looking hovered.
  • On touch screens, the field and each remove button reach past what you see to a hit area at least 44px tall, and each option is at least 44px tall (WCAG 2.5.5).
  • On touch screens, the text is at least 16px, whatever the size, since Safari on iOS zooms in when a control with smaller text gets focus, as with the Input.

Small screens

The panel is as wide as the field, never wider than the screen less 32px, and never taller than 16rem or the screen less 32px, and it scrolls inside that. It flips above the field when there's no room below, and when neither side has room, it shrinks on the bigger one, as the Popover does (WCAG 1.4.10). Browsers without CSS anchor positioning (Chrome and Edge before 129, Firefox before 147 and Safari before 26) get a script that does the same.

Motion

The arrow turns and the panel fades in over 150ms, unless people have asked for less motion (WCAG 2.3.3).

More contrast

When someone asks for more contrast (prefers-contrast: more), as with Increase contrast on macOS and iOS, the borders of the field and the panel are drawn in the text colour at full strength, rather than at 55% and 20%.

Forced colours

Windows contrast themes replace every colour with the user's own, and drop backgrounds and shadows. The focus ring, the highlighted option and a focused remove button are drawn with Highlight, the panel and each tag get a border in CanvasText, and a disabled field or option uses GrayText.

Colour scheme

The panel's background is Canvas, and the text reads --color--neutral-text, which uses light-dark(). The borders, tags and highlights mix that text colour, so the Select follows color-scheme and adapts to your light and dark themes on its own, as long as they set it.

Text size and spacing

Heights are minimums in rem, and tags wrap onto more lines, so the field grows with its text instead of clipping it (WCAG 1.4.4). A long value ends in an ellipsis, while options wrap, as Long and empty explains.

Languages and direction

The labels for removing and for the count come through removeLabel, removedLabel and countLabel, so they can be translated, and the Select uses logical properties throughout, so it mirrors in right-to-left layouts.

UX Checklist

RuleLevelScopeRequirementBasisCheck
select/combobox-patternMustComponentThe control is a <button role="combobox"> linked to a listbox with aria-expanded, aria-controls and aria-activedescendantWAI-ARIA, APG, ARIA in HTMLUnit test, ARIA snapshot
select/has-a-nameMustBothIt has a name, from a Field's label or an aria-label, and the panel has the same nameWCAG 4.1.2 (A), 3.3.2 (A)Unit test
select/reads-its-valueMustComponentThe control's text is its value, as a label, a list of labels or a count past threeWCAG 4.1.2 (A)Unit test, ARIA snapshot
select/label-press-focusesMustComponentA press on its label gives it focus without opening the panelHTML, Open ComponentsUnit test
select/keyboardMustComponentEvery key in the keyboard tables does what they sayWCAG 2.1.1 (A), APGUnit test, Keyboard
select/typeaheadMustComponentLetters typed in quick succession search the enabled options, spaces included, and one letter repeated cycles through themAPGUnit test
select/focus-staysMustComponentDOM focus stays on the control, and aria-activedescendant follows the highlighted optionAPG, WCAG 2.4.3 (A)Unit test
select/tab-commitsShouldComponentTab picks the highlighted single option, or keeps what's toggled, and moves onAPGUnit test
select/escapeMustComponentEscape closes the panel without changing the value and stops thereOpen Components, APGUnit test
select/light-dismissMustComponentA press outside closes it, and a press on the control while it's open doesn't reopen itHTMLUnit test
select/remove-tagMustComponentA value can be removed by pointer and by keyboard, with Backspace on the closed control tooWCAG 2.1.1 (A)Unit test
select/focus-after-removeMustComponentFocus goes to the next remove button, or the control when there's none after itWCAG 2.4.3 (A)Unit test
select/announce-removalShouldComponentA removed value is announced through a polite live regionWCAG 4.1.3 (AA)Unit test, Screen reader
select/form-submitMustComponentIt submits under its name in both renderings, several values includedHTMLUnit test
select/form-resetMustComponentA form's reset returns it to its first value, or to nothing, in both renderingsHTMLUnit test
select/requiredMustComponentrequired reports a missing value in both renderings, and focus reaches the controlHTMLUnit test
select/validation-focusMustComponentFocus on the hidden <select> moves to the control in the same handlerHTML, Open ComponentsUnit test
select/no-auto-pickMustComponentWith no value it starts blank, submits nothing and fires nothing on mountOpen Components, beyond HTMLUnit test
select/change-eventMustComponentA selection in the panel fires a bubbling change from the <select>HTMLUnit test
select/disabledMustComponentdisabled goes on both controls, removes the tags' buttons and leaves its formHTMLUnit test
select/read-onlyMustComponentreadonly keeps the value in its form, opens nothing and turns off the fallbackOpen ComponentsUnit test
select/native-fallbackMustComponentnativeOnTouch shows the <select> on a coarse primary pointer, with the same value, form participation, validation and Field wiringHTML, Open ComponentsUnit test, Emulation
select/no-hydration-mismatchMustComponentBoth renderings are in the markup, CSS chooses what paints and the script moves only attributes, so there's no mismatch or shiftOpen ComponentsUnit test, Emulation
select/in-a-dialogMustBothRendered inside a Dialog, its panel opens on top and Escape closes only the panelHTML, Open ComponentsUnit test
select/panel-fitsMustComponentThe panel flips and shrinks to stay inside the screen, down to 320px wideWCAG 1.4.10 (AA)Emulation, 200% zoom
select/touch-text-sizeShouldComponentOn touch screens its text is at least 16px, so Safari on iOS doesn't zoom inOpen ComponentsEmulation
select/reduced-motionShouldComponentThe arrow and the panel don't animate for people who asked for less motionWCAG 2.3.3 (AAA)Emulation
select/forced-colorsMustComponentFocus, the highlight, tags and disabled options stay visible in forced colours modeOpen ComponentsEmulation
select/right-componentShouldUsageIt's for picking out of many, while a few options use radios and a few with several answers use checkboxesOpen ComponentsReview

FAQ

Select or something else?

We recommend showing options on the page whenever they fit, because people can compare them without opening anything. The GOV.UK Design System goes further and calls a select a last resort in public-facing services, as research shows some people find them very hard to use. So try to ask for fewer options first, and reach for the Select when there are too many to lay out.

When you need to…Use
Pick one option out of a few, which fit on the screenRadio Group
Pick any number of options out of a fewCheckbox Group
Pick one option, or several, out of manySelect
Type to filter a long listCombobox
Run a few actions behind one buttonDropdown Menu

The Combobox draws its panel, options, highlight and tags with the Select's own code and tokens, with a text field for typing to filter. The Dropdown Menu, on our Roadmap, will reuse its placement, typeahead and highlight, with menu and menuitem roles and no value of its own.

Copyright © 2026