# Combobox

A box to type in that suggests matches from a list, for one choice or several: the input’s well, the dropdown panel under it, and each pick a chip.

Block: `smbk-combobox`. Status: experimental. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `selection` | `single`, `multiple` | `single` | One choice, shown as text in the box, or several, each kept as a removable chip before the text, with a tick box on every option. Figma calls this "Mode". |
| `size` | `md`, `sm` | `md` | The input’s two heights, with the dropdown’s items at the same size when open. Small text never goes under 16 px on a phone. |
| `error` | on / off | off | The value is invalid. Draws the danger border, kept while focused, and the error icon before the caret. |

Parts: `smbk-combobox__control`, `smbk-combobox__lead`, `smbk-combobox__values`, `smbk-combobox__chip`, `smbk-combobox__input`, `smbk-combobox__clear`, `smbk-combobox__caret`, `smbk-combobox__list`, `smbk-combobox__options`, `smbk-combobox__option`, `smbk-combobox__option-lead`, `smbk-combobox__option-label`, `smbk-combobox__option-detail`, `smbk-combobox__option-badge`, `smbk-combobox__check`, `smbk-combobox__status`, `smbk-combobox__status-icon`, `smbk-combobox__announcement`

## Markup

```html
<span class="smbk-combobox smbk-combobox--selection-single smbk-combobox--size-md">
  <span class="smbk-input smbk-input--size-md smbk-combobox__control">
    <span class="smbk-combobox__values">
      <input
        id="city"
        aria-label="City"
        aria-describedby="react-aria-combobox-1 react-aria-combobox-2 react-aria-combobox-3"
        type="text"
        aria-autocomplete="list"
        autoComplete="off"
        placeholder="Search cities"
        autoCorrect="off"
        spellCheck="false"
        tabindex="0"
        role="combobox"
        aria-expanded="false"
        class="smbk-input__control smbk-combobox__input"
        value=""
      >
    </span>
    <button
      type="button"
      class="smbk-combobox__caret"
      tabindex="-1"
      data-react-aria-pressable="true"
      id="react-aria-combobox-4"
      aria-label="Show suggestions"
      aria-haspopup="listbox"
      aria-expanded="false"
    >
      <i
        class="ph ph-caret-down smbk-icon smbk-icon--size-md smbk-input__icon"
        aria-hidden="true"
      ></i>
    </button>
  </span>
  <div class="smbk-dropdown__layer"><div class="smbk-dropdown smbk-dropdown--size-md smbk-dropdown--align-start smbk-combobox__list" hidden="" data-scrollbar=""><ul id="react-aria-combobox-5" role="listbox" aria-label="City" class="smbk-combobox__options" data-highlight="pointer"><li id="react-aria-combobox-5-0" role="option" data-key="aveiro" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Aveiro</span></li><li id="react-aria-combobox-5-1" role="option" data-key="braga" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Braga</span></li><li id="react-aria-combobox-5-2" role="option" data-key="coimbra" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Coimbra</span></li><li id="react-aria-combobox-5-3" role="option" data-key="evora" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Évora</span></li><li id="react-aria-combobox-5-4" role="option" data-key="faro" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Faro</span></li><li id="react-aria-combobox-5-5" role="option" data-key="funchal" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Funchal</span></li><li id="react-aria-combobox-5-6" role="option" data-key="guimaraes" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Guimarães</span></li><li id="react-aria-combobox-5-7" role="option" data-key="lagos" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Lagos</span></li><li id="react-aria-combobox-5-8" role="option" data-key="leiria" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Leiria</span></li><li id="react-aria-combobox-5-9" role="option" data-key="lisbon" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Lisbon</span></li><li id="react-aria-combobox-5-10" role="option" data-key="lisburn" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Lisburn</span></li><li id="react-aria-combobox-5-11" role="option" data-key="liskeard" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Liskeard</span></li><li id="react-aria-combobox-5-12" role="option" data-key="lismore" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Lismore</span></li><li id="react-aria-combobox-5-13" role="option" data-key="ponta-delgada" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Ponta Delgada</span></li><li id="react-aria-combobox-5-14" role="option" data-key="porto" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Porto</span></li><li id="react-aria-combobox-5-15" role="option" data-key="sintra" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Sintra</span></li><li id="react-aria-combobox-5-16" role="option" data-key="tavira" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Tavira</span></li><li id="react-aria-combobox-5-17" role="option" data-key="viana-do-castelo" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Viana do Castelo</span></li><li id="react-aria-combobox-5-18" role="option" data-key="viseu" aria-selected="false" class="smbk-dropdown__item smbk-combobox__option"><span class="smbk-dropdown__text smbk-combobox__option-label">Viseu</span></li></ul></div><span class="smbk-combobox__announcement" role="status"></span></div>
</span>
```

## React

```tsx
import { Combobox } from './design-system/components/combobox/combobox.js';

<Combobox selection="single" size="md" id="city" aria-label="City" placeholder="Search cities" options={[{ value: "aveiro", label: "Aveiro" }, { value: "braga", label: "Braga" }, { value: "coimbra", label: "Coimbra" }, { value: "evora", label: "Évora" }, { value: "faro", label: "Faro" }, { value: "funchal", label: "Funchal" }, { value: "guimaraes", label: "Guimarães" }, { value: "lagos", label: "Lagos" }, { value: "leiria", label: "Leiria" }, { value: "lisbon", label: "Lisbon" }, { value: "lisburn", label: "Lisburn" }, { value: "liskeard", label: "Liskeard" }, { value: "lismore", label: "Lismore" }, { value: "ponta-delgada", label: "Ponta Delgada" }, { value: "porto", label: "Porto" }, { value: "sintra", label: "Sintra" }, { value: "tavira", label: "Tavira" }, { value: "viana-do-castelo", label: "Viana do Castelo" }, { value: "viseu", label: "Viseu" }]} />
```

## Stylesheet

```css
/* smbk-combobox — Combobox. Figma "Combobox" 7109:2214 over "Combobox popup" 7109:1317 and "List status row"
 * 7109:1161. A box to type in that suggests matches from a list, for one choice or several.
 * Figma builds it on other components, and so does the code: the well carries smbk-input, so its chrome, hover,
 * focus, error, read-only, disabled and small size, its caret and its error icon are the input's own; each pick
 * is a chip at its small size, removable; the open list is the dropdown panel with every option a dropdown item
 * at the combobox's size, ticking with the checkbox's small box when it takes several; the loading row turns the
 * spinner; the clear cross is the search field's. This stylesheet adds only what differs: a well that grows to
 * hold wrapped chips, the text after them, the caret's press box, the active option, and the status row. */

.smbk-combobox {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  width: 100%;
}

/* ---- the well: the input's, grown for chips that wrap. Its block padding sets one row of chips, as tall as a
   large mark, in the middle of the input's height less its edge, so a single row keeps that height exactly and
   each wrapped row adds its own. Two classes, so it wins over the input's fixed height whatever order the
   stylesheets load in. ---- */

.smbk-input.smbk-combobox__control {
  height: auto;
  min-height: var(--smbk-size-control-md);
  padding-block: calc((var(--smbk-size-control-md) - var(--smbk-size-mark-lg)) / 2 - var(--smbk-border-width-hairline));
}

.smbk-combobox--size-sm .smbk-input.smbk-combobox__control {
  min-height: var(--smbk-size-control-sm);
  padding-block: calc((var(--smbk-size-control-sm) - var(--smbk-size-mark-lg)) / 2 - var(--smbk-border-width-hairline));
}

/* The chips and the text, one row that wraps: chips sit the extra-small step apart either way, and the text
   keeps room for about eight characters before it moves to a line of its own. */
.smbk-combobox__values {
  display: flex;
  flex: 1 1 auto;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--smbk-space-stack-xs) var(--smbk-space-inline-xs);
  min-width: 0;
}

.smbk-input__control.smbk-combobox__input {
  flex: 1 1 var(--smbk-size-80);
  min-width: 0;
}

/* After the last chip the text keeps the input's own gap, as Figma's Leading content slot does. */
.smbk-combobox__chip + .smbk-combobox__input {
  margin-inline-start: calc(var(--smbk-space-inline-md) - var(--smbk-space-inline-xs));
}

.smbk-combobox--size-sm .smbk-combobox__chip + .smbk-combobox__input {
  margin-inline-start: calc(var(--smbk-space-inline-sm) - var(--smbk-space-inline-xs));
}

/* A chip never runs past the well: a long name is cut short with an ellipsis, across only. */
.smbk-combobox__chip {
  max-width: 100%;
  min-width: 0;
}

.smbk-combobox__chip .smbk-chip__label {
  min-width: 0;
  overflow-x: clip;
  overflow-y: visible;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The caret is the input's icon part, so it takes the field's icon size and color; its button gives it a
   24 px box, the least a press needs, reaching past the icon into the gap and the padding, so the caret sits
   where Figma draws it. It is out of the Tab order: the arrow keys open the list. */
.smbk-combobox__caret {
  box-sizing: border-box;
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: var(--smbk-size-target-min);
  height: var(--smbk-size-target-min);
  margin: calc((var(--smbk-size-icon-md) - var(--smbk-size-target-min)) / 2);
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
  appearance: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

.smbk-combobox--size-sm .smbk-combobox__caret {
  margin: calc((var(--smbk-size-icon-sm) - var(--smbk-size-target-min)) / 2);
}

.smbk-combobox__caret:disabled {
  cursor: not-allowed;
}

/* The clear cross is the search field's (smbk-search__clear) and keeps its look. */
.smbk-combobox__clear {
  flex-shrink: 0;
}

/* ---- the open list: the dropdown panel under the well, as wide as the well ----
   Without script the panel hangs from the combobox, under the well whatever its height; lifted over the page,
   the dropdown's layer takes it and the wrapper sets its place and width. It keeps the well's width, narrower or
   wider than a dropdown, and a long choice wraps inside it. A list longer than the list height, or than its room,
   stops there and scrolls inside itself, the dropdown panel's own rule. */

.smbk-combobox .smbk-dropdown.smbk-combobox__list {
  z-index: var(--smbk-layer-overlay);
  width: 100%;
  min-width: 0;
  max-width: none;
}

.smbk-combobox__options {
  display: flex;
  flex-direction: column;
  gap: var(--smbk-space-0);
  margin: 0;
  padding: 0;
  list-style: none;
  outline: none;
}

.smbk-combobox__options > * {
  flex-shrink: 0;
}

/* The active option, which the arrow keys move while focus stays in the box, takes the dropdown item's hover
   fill. While the box has focus and the keys moved it, it also carries the focus ring, drawn just inside its
   edge, so it is never shown by its fill alone; an option under the pointer takes the hover fill alone. */
/* Two classes, so the fill outweighs the dropdown item's own clear background, which the bundle loads after this
   stylesheet. */
.smbk-dropdown__item.smbk-combobox__option--active {
  background: var(--smbk-color-surface-hover);
}

/* While the pointer's fill shows (the dropdown's glide), it is the active option, so the option draws no second one. */
.smbk-combobox__list[data-glide="lit"] .smbk-combobox__option--active {
  background: transparent;
}

/* Any option, in a group or not: the options of a group sit in the group's own list. */
.smbk-combobox:focus-within [data-highlight="keys"] .smbk-combobox__option--active {
  outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
  outline-offset: calc(-1 * var(--smbk-focus-ring-width));
}

/* ---- what an option carries besides its label: a color swatch before it, at the item's icon size (the dropdown's
   icon part draws an icon there), then quieter words, the select's second line set after the label instead of under
   it, and a small badge, before the tick. The label takes the room; the words after it are cut short with an
   ellipsis before the badge moves. Groups are the select's: the dropdown's label over the dropdown's group, a line
   between them. ---- */

.smbk-combobox__option-detail {
  flex: 0 1 auto;
  min-width: 0;
}

.smbk-combobox__option-badge {
  flex-shrink: 0;
}

/* A disabled option dims its swatch and its badge with its words. */
.smbk-combobox__option[aria-disabled="true"] :is(.smbk-color-swatch.smbk-combobox__option-lead, .smbk-combobox__option-badge) {
  opacity: var(--smbk-opacity-disabled);
}

/* ---- the status row: what the list says instead of options, in words: no results, loading with the turning
   ring, or an error with its icon. Body SM in the secondary text color at both sizes; one line keeps the
   medium control height. ---- */

.smbk-combobox__status {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: var(--smbk-space-inline-md);
  min-height: var(--smbk-size-control-md);
  margin: 0;
  padding: var(--smbk-space-inset-lg);
  color: var(--smbk-color-text-subtle);
  font-family: var(--smbk-typography-body-sm-font-family);
  font-size: var(--smbk-typography-body-sm-font-size);
  font-weight: var(--smbk-typography-body-sm-font-weight);
  line-height: var(--smbk-typography-body-sm-line-height);
  letter-spacing: var(--smbk-typography-body-sm-letter-spacing);
}

.smbk-combobox--size-sm .smbk-combobox__status {
  padding-block: var(--smbk-space-inset-md);
}

/* The spinner keeps its small subtle ring and its words, set the row's gap apart. */
.smbk-combobox__status .smbk-spinner {
  gap: var(--smbk-space-inline-md);
}

.smbk-combobox__status .smbk-spinner__label {
  white-space: normal;
}

.smbk-combobox__status--type-error {
  color: var(--smbk-color-feedback-danger-text-on-subtle);
}

.smbk-combobox__status-icon {
  flex-shrink: 0;
  color: var(--smbk-color-feedback-danger-icon);
  font-size: var(--smbk-size-icon-sm);
}

/* What a screen reader hears as the list loads or fails, for screen readers only. */
/* .smbk-combobox__announcement: hidden from view, kept for screen readers and the keyboard, by the shared rule in visually-hidden/visually-hidden.css. */

/* ---- touch: the picks, the clear cross and the caret take invisible press areas ----
   On a touch screen the caret takes an invisible area, centred on it, out to the comfortable target, as the clear
   cross and each pick's remove cross do; between the two buttons, and toward the text before them, each area reaches
   only halfway into the gap. In the row of picks, which wraps, every remove cross and the text reach only halfway into
   the row's gaps, so each pick stays one press. Nothing drawn moves and the well keeps its height. */

@media (pointer: coarse) {
  .smbk-combobox__caret {
    position: relative;
  }

  .smbk-combobox__caret::after {
    content: '';
    position: absolute;
    inset: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
  }

  .smbk-input > .smbk-combobox__caret::after {
    inset-inline-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (var(--smbk-space-inline-md) + (var(--smbk-size-icon-md) - var(--smbk-size-target-min)) / 2) / -2);
  }

  .smbk-input > .smbk-combobox__clear:has(+ .smbk-combobox__caret)::after {
    inset-inline-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (var(--smbk-space-inline-md) + var(--smbk-size-icon-md) - var(--smbk-size-target-min)) / -2);
  }

  .smbk-input > .smbk-combobox__clear + .smbk-combobox__caret::after {
    inset-inline-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (var(--smbk-space-inline-md) + var(--smbk-size-icon-md) - var(--smbk-size-target-min)) / -2);
  }

  .smbk-combobox--size-sm .smbk-input > .smbk-combobox__caret::after {
    inset-inline-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (var(--smbk-space-inline-sm) + (var(--smbk-size-icon-sm) - var(--smbk-size-target-min)) / 2) / -2);
  }

  .smbk-combobox--size-sm .smbk-input > .smbk-combobox__clear:has(+ .smbk-combobox__caret)::after {
    inset-inline-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (var(--smbk-space-inline-sm) + var(--smbk-size-icon-sm) - var(--smbk-size-target-min)) / -2);
  }

  .smbk-combobox--size-sm .smbk-input > .smbk-combobox__clear + .smbk-combobox__caret::after {
    inset-inline-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (var(--smbk-space-inline-sm) + var(--smbk-size-icon-sm) - var(--smbk-size-target-min)) / -2);
  }

  .smbk-combobox__values .smbk-chip__remove::after {
    inset-inline: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-xs) / -2);
    inset-block: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-stack-xs) / -2);
  }

  .smbk-combobox__values > .smbk-input__control {
    padding-block: max(0%, min((var(--smbk-size-target-comfortable) - 1lh) / 2, var(--smbk-space-stack-xs) / 2));
    margin-block: min(0%, max((1lh - var(--smbk-size-target-comfortable)) / 2, var(--smbk-space-stack-xs) / -2));
  }
}

/* ---- high contrast (forced colors) ---- */
/* In Windows high-contrast mode the browser drops fills and shadows; this puts the meaning back with the
   user's own colors. Nothing here applies outside that mode. */

@media (forced-colors: active) {
  /* The active option in the open list. */
  .smbk-dropdown__item.smbk-combobox__option--active {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
  }

  .smbk-combobox__option--active .smbk-dropdown__icon,
  .smbk-combobox__option--active .smbk-select__option-description {
    color: inherit;
  }
}
```

## Tokens it reads

```json
{
  "size": {
    "80": {
      "$type": "dimension",
      "$value": "80px",
      "$description": "Raw size step, 80 px. Only for fixed sizes inside a component; semantic sizes (size/control, icon, mark) point here."
    },
    "control": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.40}}",
        "$description": "Height of a medium control, where controls insert by default: 40 comfortable, 32 compact. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{size.32}}",
        "$description": "Height of a small control: 32 comfortable, 28 compact. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      }
    },
    "mark": {
      "lg": {
        "$type": "dimension",
        "$value": "{{size.24}}",
        "$description": "Small fixed marks inside components (dots, bars, keys), large."
      }
    },
    "target": {
      "min": {
        "$type": "dimension",
        "$value": "{{size.24}}",
        "$description": "Smallest tap area allowed (24, WCAG 2.2)."
      },
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    },
    "icon": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.20}}",
        "$description": "Icon box, medium."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{size.16}}",
        "$description": "Icon box, small."
      }
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    }
  },
  "space": {
    "0": {
      "$type": "dimension",
      "$value": "0px",
      "$description": "Raw space step, 0 px. Only for fixed distances inside a component; spacing that follows density uses space/inset, stack or inline."
    },
    "stack": {
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things stacked one above another, extra small."
      }
    },
    "inline": {
      "xs": {
        "$type": "dimension",
        "$value": "{{space.4}}",
        "$description": "Gap between things side by side, extra small."
      },
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things side by side, medium."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{space.6}}",
        "$description": "Gap between things side by side, small."
      }
    },
    "inset": {
      "lg": {
        "$type": "dimension",
        "$value": "{{space.12}}",
        "$description": "Padding inside a component, large."
      },
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Padding inside a component, medium."
      }
    }
  },
  "layer": {
    "overlay": {
      "$type": "number",
      "$value": 1200
    }
  },
  "color": {
    "surface": {
      "hover": {
        "$type": "color",
        "$value": "{{color.alpha.grey-800.15}}",
        "$description": "A row or option under the pointer or the keyboard highlight: menu options, table rows. Translucent, so it works on any surface."
      }
    },
    "border": {
      "focus": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Keyboard focus ring on controls."
      }
    },
    "text": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Secondary text: descriptions, metadata."
      }
    },
    "feedback": {
      "danger": {
        "text-on-subtle": {
          "$type": "color",
          "$value": "{{color.red.700}}",
          "$description": "Danger messages: text on the pale fill."
        },
        "icon": {
          "$type": "color",
          "$value": "{{color.red.600}}",
          "$description": "The danger tone’s own icon (an alert sign), so the tone is never shown by colour alone."
        }
      }
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      }
    }
  },
  "opacity": {
    "disabled": {
      "$type": "number",
      "$value": 0.5,
      "$description": "Fade for disabled pictures and media (50%)."
    }
  },
  "typography": {
    "body-sm": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "14px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  }
}
```

## Purpose

A box to type in that suggests matches from a list, for one choice or several: a city, a person, a tag. Typing narrows the list under the box, and the reader picks from it with the keys or a press. It differs from the select, which opens a fixed list to choose from without typing, and from the search field, which takes free text and suggests nothing.

## Anatomy

- The block, holding the box and, while open, the list.
- The control part, the input's well: it carries the input's classes, so its chrome, hover, focus, error, read-only and disabled looks and its two sizes are the input's.
- The lead part, with one choice whose option has a color: the chosen option's color swatch before the text, in the input's leading content. An option with an icon shows the icon there instead, as the input's icon.
- The values part, one row inside the well that wraps: the chips, then the text.
- The chip part, one per pick when the combobox takes several: a chip at its small size, with its remove cross, named "Remove" and the pick's label.
- The input part, the text box typed in, which is the combobox for a screen reader.
- In error, the input's error icon, a warning sign before the caret.
- The clear part, the search field's cross, shown while the box holds text or a choice. It empties the box and removes the choice.
- The caret part, the input's icon, in a press box that opens and closes the list.
- The list part, the dropdown panel itself (it carries the dropdown's classes at the combobox's size). Inside it, the options part, a listbox of option parts, each a dropdown item with its option label part and, for one choice, the check part on the chosen one; for several, the checkbox's small box on every option, ticked on each pick. Before the label an option may carry the option lead part, a color swatch at the item's icon size or the dropdown's icon; after it, the option detail part, quieter words in the select's second-line style, and the option badge part, a small badge.
- Groups of options, each the select's group part: the dropdown's label as its heading, the dropdown's group around its options, and the dropdown's divider before each group that follows something.
- The status part, a row the list shows instead of options: no results, loading with the turning ring, or an error with its icon, always in words. Its type is a modifier: `--type-no-results`, `--type-loading`, `--type-error`.
- The announcement part, for screen readers only: says the list is loading or failed as it happens.
- The label and the help text are not the combobox's: put it in a field.

## When to use

- A long list the reader searches by name: a country among two hundred, a person in a team, a city.
- A list a page fetches as the reader types, such as a search on a server.
- Several picks from a long list, each kept visible as a chip: the tags on a post, the cities on a trip.

## When not to use

- A handful of choices. Use a [select](select.md), or [radios](radio.md) when they should all be visible.
- Free text with no list to choose from. Use an [input](input.md), or a [search field](search-field.md) for a search.
- Going somewhere rather than choosing a value, such as a site search that opens a page. Use a search field and a page of results, or the [command palette](command-palette.md).
- Several choices from a short list. Use [checkboxes](checkbox.md).

## Behavior

The box behaves as the input does: it lifts on hover, shows the field focus border when focused, and in error draws the danger border, kept while focused, and the warning icon. It comes in the input's two sizes, md and sm; the list's items follow the size.

Options can carry more than a label. A color swatch or an icon before it shows what the option is at a glance; quieter words or a badge after it add a fact, such as where a value comes from or a color's contrast. With one choice, the box shows the chosen option's swatch or icon before its label, so it says which color is chosen without opening; once the reader types something else, it goes until a choice is made again. Options can also come in groups, each under its heading, with a line between groups. Matching works through every group, and a group whose options all fail to match is left out, heading and all. The keys move from the last option of a group to the first of the next.

Typing opens the list under the box with the options that match what is typed, anywhere in the label by default, or only at its start, ignoring case and accents in the reader's language. Down and Up open the list on its first or last option, or on the chosen one, and then move the active option; focus never leaves the box. Enter picks the active option. With one choice, the list closes and the box shows the choice; with several, the pick becomes a chip before the text, the list stays open for the next, and Enter on a picked option removes it. Escape closes the list and puts the box back as it was before typing; a second Escape empties a single choice. Tab leaves and picks nothing. Leaving the box puts it back as well: what is typed is a search, never the value. The caret opens and closes the list with a press. With several picks, each chip's cross removes it, and Backspace in an empty box removes the last one.

`onActiveChange` tells the page which option is active, the one Enter would pick, before anything is picked: its value, and what made it active, the keys, the pointer moved onto it, or a mouse at rest that the list opened, changed or scrolled under. It is told null as the list closes or typing leaves no option active. A page can show the active option in a preview, such as a color on a sample, and leave out one that only came under a resting mouse, which the reader never moved to.

What is typed and the value chosen are kept apart, and each can be controlled from outside or left to the combobox. A page that finds the options itself, as a search on a server does, sets `filter` to none, passes the matches as the options as they arrive, and sets `status` to loading meanwhile, or to error when the search failed; an empty list says there are no results. The page cancels an earlier search when a newer one starts. While a search runs, keep the chosen options among the options passed, so the box can show their labels.

The open list shows over the whole page, so a part around the combobox that clips or scrolls never cuts it off, and it keeps the theme, density and text size of the place it opened from. It is as wide as the well, never taller than the system's list height or than the room it has in the window, under the well or above it when there is more room there, and stays beside the well as the page scrolls. A longer list scrolls inside itself with the system's scrollbar, a thin, see-through bar over its options that takes no room and shows while the mouse is over it or it scrolls, and a soft shade along its foot, and keeps the active option in view. With a mouse, the active option is the dropdown's sliding hover fill: it follows the pointer from option to option, lights the nearest option when the pointer is in the list's padding, where a click picks it, and steps aside while the keys move, so the keyboard's active option keeps its own focus ring. The list drops in with the dropdown's motion; under reduced motion it fades in and out without moving. The placing and the keys come from React Aria, which a React project installs beside React.

The box grows a row at a time as chips wrap, and the text keeps room for about eight characters after them. A long label on a chip is cut short with an ellipsis. A read-only combobox shows its choice or chips with no caret, no clear cross and no remove crosses, and stays in the tab order. A disabled one is dimmed and out of reach, its chips with it, and sends nothing with its form.

The value travels in hidden inputs when the combobox has a form name: one for a single choice, one for each pick. A required combobox stops its form being sent while nothing is chosen (for several, while none is picked): the box takes focus, the browser says why, and the combobox takes its error look until a choice is made. The form's reset puts the starting choice or picks back. A ref given to the React wrapper reaches the box typed in, so a page can focus it.

`defaultOpen` shows the list open from the start, as in a picture of the page; focus in the box makes it the working list.

## Accessibility

### What it does for you

- The box is a combo box with a list autocomplete that controls a listbox.
- The active option is announced through the active descendant, so focus stays in the box. While the box has focus, the active option carries the focus ring just inside its edge as well as its fill, so it is never shown by color alone.
- Each group is a group named by its heading, so a screen reader says the group as the active option moves into it.
- An option's name is its label, then its quieter words and its badge's words, each after a comma. Its swatch or icon is decoration: the label names the option.
- The number of matches is announced as the list opens and as it changes. The loading and error rows are announced as they appear; no results is said in words in the list.
- With several picks, the listbox says so, every option says whether it is picked, and each chip's cross is a button named "Remove" and the pick's label.
- While the list is open, the rest of the page is hidden from a screen reader, so its reading stays in the box and the list; it comes back as the list closes.
- The caret and the clear cross are out of the Tab order: the keys open the list and Escape empties the box. A screen reader can still reach and press them.
- An error marks the box invalid as well as drawing the danger border.

### What you must do

- Give the box a label, from a field, or with `aria-label` or `aria-labelledby`. In development, a combobox with no name logs a warning in the console.
- Put it in a field, which links its help text or error message to the box through `aria-describedby` and adds `aria-required` to the box when it is required.
- Change the words of the chips' crosses with `removeLabel` when the page is in another language.
- Name each option in its label. A swatch's color is never the only way to tell two options apart; a badge's color says nothing its words do not.
- Written by hand, give an option with a badge or quieter words an `aria-label` of its label and those words, each after a comma, so they never run together.

### How to test

- With the keyboard only, type a few letters, move with Down Arrow and Up Arrow, press Enter to pick, then press Escape. Focus stays in the box throughout.
- With a screen reader, the box is read as a combo box by its label, the number of matches is spoken as you type, and each option says whether it is picked.
- With several picks, the cross on each chip is read as "Remove" and the pick's label.
- In a list with groups, move through it with the arrow keys: each group's heading is said as the active option enters it, and each option says its badge's words.
- Trigger the loading and error rows: each is spoken as it appears.
- With text at 200% and the page 320 pixels wide, the box grows a row at a time and the list stays inside the window.
- In a forced-color mode, the active option still shows its ring.

### Keyboard

| Key | What it does |
|---|---|
| Any character | Types in the box and opens the list with the options that match. |
| Down Arrow | Opens the list on the chosen option, or the first; with the list open, moves the active option down. |
| Up Arrow | Opens the list on the chosen option, or the last; with the list open, moves the active option up. |
| Enter | Picks the active option. With several picks, Enter on a picked option removes it. |
| Escape | Closes the list and puts the box back as it was before typing. A second Escape empties a single choice. |
| Tab | Leaves the box and picks nothing. |
| Backspace | With several picks and an empty box, removes the last pick. |

## Usage rules

### Do

- Put it in a field, with a label that stays in view while the reader types.
- Say what to type in the placeholder or the help text, such as "Type a few letters".
- Write the error row's words as what went wrong and what to do.
- Group a long list under short headings the reader expects, such as a color's family.
- Keep the words after a label short: a contrast ratio, an address, a count.

### Don't

- Use a combobox for a few choices. Use a select or radios.
- Use it for navigation. Use a search field or the command palette.
- Show options that do not match what was typed.
- Give every option a badge when only a few need one: the badges stop standing out.
