Accessibility pattern · Form inputs

Multi-select with tags

The field is a combobox: typing filters a listbox, the arrow keys move a highlight while focus stays in the field, and Enter adds or removes the highlighted value. Each choice becomes a chip in a list with a remove button named after it, and a polite status says what was added or removed and how many are chosen.

WCAG criteria
7
Keyboard rules
7
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.

3 selected

Type to filter the list. In the empty field, Backspace moves to the last skill.

  • Figma
  • Accessibility
  • Typography

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
Arrow Down or Arrow UpIn the field, opens the list and moves the highlight to the next or previous skill, wrapping at the ends.
EnterAdds the highlighted skill as a chip, or removes it if it is already chosen. The text clears and focus stays in the field.
EscapeCloses the list; pressed again, clears what was typed.
BackspaceIn the empty field, moves focus to the last chip's remove button. On a chip, removes it.
Arrow Left or Arrow RightMoves between chips; Arrow Left at the start of the field goes to the last chip, and Arrow Right after the last chip comes back.
Enter or Space or DeleteOn a chip, removes it. Focus moves to the chip that takes its place, or to the field.
TabCloses the list without choosing and moves on.

Screen readers

What it announces

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

WhenExpected announcement
Focus reaches the fieldSkills, combo box, collapsed. 3 selected. Type to filter the list.
Arrow Down highlights a skillUser research, not selected, 4 of 13
Enter adds itAdded: User research. 4 selected
Backspace in the empty fieldRemove User research, button
A chip is removedRemoved: User research. 3 selected
Nothing matches what was typedNo skills match

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

<div class="ap-multi-select" data-ap-multi-select>
  <div class="ap-multi-select__top">
    <label class="ap-multi-select__label" for="multi-select-input">Skills</label>
    <p class="ap-multi-select__count" id="multi-select-count"><span data-ap-n translate="no">3</span> <span>selected</span></p>
  </div>
  <p class="ap-multi-select__hint" id="multi-select-hint">Type to filter the list. In the empty field, Backspace moves to the last skill.</p>

  <div class="ap-multi-select__field">
    <div class="ap-multi-select__control" data-ap-control>
      <ul class="ap-multi-select__chips" aria-label="Selected skills" data-ap-chips>
      <li class="ap-multi-select__chip" data-value="Figma">
        <span class="ap-multi-select__chip-text">Figma</span>
        <button type="button" class="ap-multi-select__remove" aria-label="Remove Figma"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 7l10 10M17 7 7 17"/></svg></button>
      </li>
      <li class="ap-multi-select__chip" data-value="Accessibility">
        <span class="ap-multi-select__chip-text">Accessibility</span>
        <button type="button" class="ap-multi-select__remove" aria-label="Remove Accessibility"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 7l10 10M17 7 7 17"/></svg></button>
      </li>
      <li class="ap-multi-select__chip" data-value="Typography">
        <span class="ap-multi-select__chip-text">Typography</span>
        <button type="button" class="ap-multi-select__remove" aria-label="Remove Typography"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 7l10 10M17 7 7 17"/></svg></button>
      </li>
      </ul>
      <input class="ap-multi-select__input" id="multi-select-input" type="text" role="combobox" aria-expanded="false" aria-controls="multi-select-list" aria-autocomplete="list" aria-describedby="multi-select-count multi-select-hint" autocomplete="off" spellcheck="false" placeholder="Add a skill" />
      <svg class="ap-multi-select__chevron" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 9 6 6 6-6"/></svg>
    </div>

    <div class="ap-multi-select__pop" data-ap-pop hidden>
      <ul class="ap-multi-select__list" id="multi-select-list" role="listbox" aria-label="Skills" aria-multiselectable="true">
      <li class="ap-multi-select__opt" role="option" id="multi-select-o-figma" aria-selected="true" data-value="Figma">
        <span class="ap-multi-select__box-mark" aria-hidden="true"><svg class="ap-multi-select__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg></span>
        <span class="ap-multi-select__opt-text">Figma</span>
      </li>
      <li class="ap-multi-select__opt" role="option" id="multi-select-o-accessibility" aria-selected="true" data-value="Accessibility">
        <span class="ap-multi-select__box-mark" aria-hidden="true"><svg class="ap-multi-select__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg></span>
        <span class="ap-multi-select__opt-text">Accessibility</span>
      </li>
      <li class="ap-multi-select__opt" role="option" id="multi-select-o-typography" aria-selected="true" data-value="Typography">
        <span class="ap-multi-select__box-mark" aria-hidden="true"><svg class="ap-multi-select__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg></span>
        <span class="ap-multi-select__opt-text">Typography</span>
      </li>
      <li class="ap-multi-select__opt" role="option" id="multi-select-o-research" aria-selected="false" data-value="User research">
        <span class="ap-multi-select__box-mark" aria-hidden="true"><svg class="ap-multi-select__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg></span>
        <span class="ap-multi-select__opt-text">User research</span>
      </li>
      <li class="ap-multi-select__opt" role="option" id="multi-select-o-prototyping" aria-selected="false" data-value="Prototyping">
        <span class="ap-multi-select__box-mark" aria-hidden="true"><svg class="ap-multi-select__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg></span>
        <span class="ap-multi-select__opt-text">Prototyping</span>
      </li>
      <li class="ap-multi-select__opt" role="option" id="multi-select-o-systems" aria-selected="false" data-value="Design systems">
        <span class="ap-multi-select__box-mark" aria-hidden="true"><svg class="ap-multi-select__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg></span>
        <span class="ap-multi-select__opt-text">Design systems</span>
      </li>
      <li class="ap-multi-select__opt" role="option" id="multi-select-o-illustration" aria-selected="false" data-value="Illustration">
        <span class="ap-multi-select__box-mark" aria-hidden="true"><svg class="ap-multi-select__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg></span>
        <span class="ap-multi-select__opt-text">Illustration</span>
      </li>
      <li class="ap-multi-select__opt" role="option" id="multi-select-o-motion" aria-selected="false" data-value="Motion design">
        <span class="ap-multi-select__box-mark" aria-hidden="true"><svg class="ap-multi-select__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg></span>
        <span class="ap-multi-select__opt-text">Motion design</span>
      </li>
      <li class="ap-multi-select__opt" role="option" id="multi-select-o-ia" aria-selected="false" data-value="Information architecture">
        <span class="ap-multi-select__box-mark" aria-hidden="true"><svg class="ap-multi-select__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg></span>
        <span class="ap-multi-select__opt-text">Information architecture</span>
      </li>
      <li class="ap-multi-select__opt" role="option" id="multi-select-o-copy" aria-selected="false" data-value="Copywriting">
        <span class="ap-multi-select__box-mark" aria-hidden="true"><svg class="ap-multi-select__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg></span>
        <span class="ap-multi-select__opt-text">Copywriting</span>
      </li>
      <li class="ap-multi-select__opt" role="option" id="multi-select-o-html" aria-selected="false" data-value="HTML and CSS">
        <span class="ap-multi-select__box-mark" aria-hidden="true"><svg class="ap-multi-select__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg></span>
        <span class="ap-multi-select__opt-text">HTML and CSS</span>
      </li>
      <li class="ap-multi-select__opt" role="option" id="multi-select-o-js" aria-selected="false" data-value="JavaScript">
        <span class="ap-multi-select__box-mark" aria-hidden="true"><svg class="ap-multi-select__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg></span>
        <span class="ap-multi-select__opt-text">JavaScript</span>
      </li>
      <li class="ap-multi-select__opt" role="option" id="multi-select-o-react" aria-selected="false" data-value="React">
        <span class="ap-multi-select__box-mark" aria-hidden="true"><svg class="ap-multi-select__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9"/></svg></span>
        <span class="ap-multi-select__opt-text">React</span>
      </li>
      </ul>
      <p class="ap-multi-select__none" data-ap-none hidden>No skills match</p>
    </div>
  </div>

  <p class="ap-multi-select__say" role="status" data-ap-say></p>
</div>

WCAG 2.2

What it meets

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

  • 1.3.1 Info and Relationships Level A

    The chips are a named list, so a screen reader says how many there are, and the field's label and hint are tied to it in code.

  • 2.1.1 Keyboard Level A

    Choosing, removing and moving between chips all work from the keyboard; the pointer is never required.

  • 2.4.3 Focus Order Level A

    When a chip goes, focus moves to the chip that takes its place or back to the field, never to the top of the page.

  • 2.4.7 Focus Visible Level AA

    The field shows a ring while it has focus, each remove button has its own, and the highlighted option is outlined.

  • 2.5.8 Target Size (Minimum) Level AA

    Each remove button is at least 24 by 24 pixels and sits apart from its neighbours.

  • 4.1.2 Name, Role, Value Level A

    The field is a combobox with aria-expanded and aria-activedescendant; options carry aria-selected, and each remove button is named after its skill.

  • 4.1.3 Status Messages Level AA

    Adding, removing and an empty search are reported in a polite status message without moving focus.

Usage

When to use it

Use it

  • Choosing several values from a long, known list, like skills, tags, recipients or cities.
  • When the chosen values need to stay visible and easy to remove one at a time.

Use something else

  • Fewer than about eight choices: a group of checkboxes shows them all and needs no typing.
  • Free text people make up as they go, like keywords: a plain text field is simpler.
  • Choosing exactly one value: use a combobox or a select.

Common failures

How it usually goes wrong

  • Remove buttons named only "×"

    A screen reader user hears "times, button" six times and cannot tell which chip each one removes. Each button here is named "Remove" plus its skill.

  • Focus lost when a chip is removed

    Deleting the focused button drops focus to the page, so keyboard users start again from the top. Focus moves to the next chip or back to the field.

  • Silent changes

    A chip appearing or vanishing makes no sound. A polite status says what was added or removed and the new count.

  • The highlight moves focus out of the field

    Moving real focus into the list stops people typing. aria-activedescendant points at the highlighted option while the caret stays put.

  • Chosen values shown by color alone

    A tinted row is easy to miss. Chosen options also carry a ticked box and aria-selected, and the chips repeat them in words.

  • Chips that cannot be reached

    Chips drawn as decoration, with remove icons that only respond to a click, cannot be removed from the keyboard. Each one is a real button.

Notes

Building it

  • The listbox has aria-multiselectable="true", and aria-selected marks every chosen skill, so Enter on a chosen one removes it again.
  • Keep the English value in data-value and build chip names from it: the script then writes only strings it knows, and translation happens in one place.
  • Typing highlights the first match so Enter adds it at once; with an empty field, nothing is highlighted until the arrow keys are used.
  • The live count sits in the field's description too, so it is heard when focus arrives, not only when it changes.
  • A click anywhere in the field box, between the chips too, puts focus in the text input and opens the list, so the box behaves as one field.

Sources: WAI-ARIA Authoring Practices: Combobox · WAI-ARIA Authoring Practices: Listbox

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