Accessibility pattern · Form inputs

Select

A native select already works everywhere: with every screen reader, with the keyboard, and as the system's own picker on a phone. Style it rather than replace it. Where appearance: base-select is supported the open list takes the same design; elsewhere the closed box is styled and the browser draws the list.

WCAG criteria
7
Keyboard rules
6
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Delivery details

Where should we send your order?

We deliver to these cities in two to four working days.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Tab or ShiftTabMoves to and from each select.
Space or AltArrow DownOpens the list with the chosen option, or the first city, in focus.
Arrow Down or Arrow UpIn the open list, moves to the next or previous city, across the region groups.
EnterChooses the focused city and closes the list.
EscapeCloses the list without changing the choice.
A–ZTyping the first letters of a city jumps to it, with the list open or closed.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches the city selectCity, combo box, Choose a city, required. We deliver to these cities in two to four working days.
Space opens the listNorth, group. Chandigarh, option, 1 of 14
Enter chooses PuneCity, combo box, Pune
Save is pressed with no cityCity, combo box, Choose a city, invalid entry, required. We deliver to these cities in two to four working days. Choose a city for delivery.
The details are savedSaved: Pune

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<form class="ap-select" data-ap-select novalidate>
  <h3 class="ap-select__name">Delivery details</h3>
  <p class="ap-select__lead">Where should we send your order?</p>

  <div class="ap-field">
    <label class="ap-label" for="select-city">City</label>
    <p class="ap-hint" id="select-city-hint">We deliver to these cities in two to four working days.</p>
    <div class="ap-select__box">
      <select class="ap-select__control" id="select-city" name="city" required aria-describedby="select-city-hint">
        <option value="" disabled selected>Choose a city</option>
        <optgroup label="North">
          <option value="chandigarh">Chandigarh</option>
          <option value="delhi">Delhi</option>
          <option value="jaipur">Jaipur</option>
          <option value="lucknow">Lucknow</option>
        </optgroup>
        <optgroup label="West">
          <option value="ahmedabad">Ahmedabad</option>
          <option value="mumbai">Mumbai</option>
          <option value="pune">Pune</option>
        </optgroup>
        <optgroup label="South">
          <option value="bengaluru">Bengaluru</option>
          <option value="chennai">Chennai</option>
          <option value="hyderabad">Hyderabad</option>
          <option value="kochi">Kochi</option>
        </optgroup>
        <optgroup label="East">
          <option value="bhubaneswar">Bhubaneswar</option>
          <option value="guwahati">Guwahati</option>
          <option value="kolkata">Kolkata</option>
        </optgroup>
      </select>
      <svg class="ap-select__chevron" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
    </div>
    <p class="ap-error" id="select-city-error" hidden>
      <svg class="ap-select__error-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5.5M12 16.5h.01"/></svg>
      <span>Choose a city for delivery.</span>
    </p>
  </div>

  <div class="ap-field">
    <label class="ap-label" for="select-slot">Delivery time <span class="ap-select__optional">(optional)</span></label>
    <div class="ap-select__box">
      <select class="ap-select__control" id="select-slot" name="slot">
        <option value="any" selected>Any time, 9 am to 9 pm</option>
        <option value="morning">Morning, 9 am to 12 noon</option>
        <option value="afternoon">Afternoon, 12 noon to 4 pm</option>
        <option value="evening">Evening, 4 pm to 9 pm</option>
      </select>
      <svg class="ap-select__chevron" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
    </div>
  </div>

  <div class="ap-select__actions">
    <button type="submit" class="ap-btn ap-btn--primary">Save delivery details</button>
  </div>
  <p class="ap-select__status" role="status"></p>
</form>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Each select has a label element, its hint and error are tied to it with aria-describedby, and optgroups group the cities by region in a way assistive technology can read.

  • 1.4.11 Non-text Contrast Level AA

    The select's edge and chevron clear 3:1 against the card, so the control can be found without its label.

  • 2.1.1 Keyboard Level A

    Opening, moving through and choosing are the browser's own keys, the same in every select on the web.

  • 2.4.7 Focus Visible Level AA

    The select shows a two-pixel focus ring, and so does each option in the styled list.

  • 3.3.1 Error Identification Level A

    Saving without a city marks the select invalid and shows the reason in text beside it, with an icon, and focus goes to the select.

  • 3.3.2 Labels or Instructions Level A

    The label says what to choose, the hint says what happens next, and the optional field says it is optional.

  • 4.1.2 Name, Role, Value Level A

    A native select exposes its role, name, value and required state with no ARIA added.

Usage

When to use it

Use it

  • Choosing one option from a list of about five to fifteen, where people know what they are looking for.
  • Forms on phones, where the system's own picker is the quickest way to choose.

Use something else

  • Two to five options people should compare at a glance: show them as radio buttons.
  • Long lists, like every city in the country: use a combobox people can type into.
  • Choosing several options: use checkboxes or a multi-select listbox.

Common failures

How it usually goes wrong

  • A custom dropdown instead of a select

    A div that opens a list of divs has to rebuild the keyboard, focus, typing to jump, the phone picker and screen reader support, and usually misses some. A native select has them all.

  • A placeholder people can choose

    If "Choose a city" can be selected and submitted, the form receives an empty value that looks like an answer. Here it is disabled, and the required check rejects it.

  • No label, only the placeholder

    Once a city is chosen, the placeholder is gone and nothing says what the field is. The label stays above the select.

  • Error shown only by a red edge

    A red border is invisible to many people and silent to screen readers. The error here is text, with an icon, tied to the select and announced with it.

  • The arrow drawn by removing it

    appearance: none with no replacement leaves a box that does not look like it opens. The chevron here is drawn over the select and clears 3:1.

Notes

Building it

  • appearance: base-select is set inside @supports, on both the select and its ::picker(select); browsers without it keep the native list and still get the styled box.
  • The chevron is an SVG laid over the select with pointer-events: none, so a click on it still opens the select.
  • The placeholder option has value="", disabled and selected: the required attribute then counts it as no answer, and nobody can choose it again.
  • Optgroups are read as groups by screen readers and drawn as labelled sections in the list; they cannot be chosen themselves.
  • The form has novalidate so the error is written by the page, in the page's words and style, instead of the browser's bubble.

Sources: HTML: the select element · Chrome for Developers: customizable select

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction