# Combobox

> Single-select combobox with portaled dropdown, shared with MultiCombobox.

- Status: stable
- Group: form
- Page: https://www.digitaltableteur.com/design-system/components/combobox
- Contract: https://www.digitaltableteur.com/contracts/v1/Combobox.contract.json

## When to use

Combobox is the styled single-select for editorial forms: a button trigger that opens a portaled listbox with the full APG keyboard model. It is controlled; pair value with onValueChange. It owns its field chrome (label, error, helperText), and shares its look and dropdown behavior with MultiCombobox. Reach for it when the native Select styling is not enough; the option list stays flat strings only.

## When not to use

- Do not nest inside containers with `overflow: hidden` without portaling (dropdown renders on `document.body`).
- Do not use Combobox for free text with suggestions; it only emits option values (no free-solo mode). Use TextInput plus your own suggestion list.
- Do not use it for multiple selection; MultiCombobox owns chips and type-to-filter.

## Look-alikes

- MultiCombobox: Do not use it for multiple selection; MultiCombobox owns chips and type-to-filter.
- TextInput: Do not use Combobox for free text with suggestions; it only emits option values (no free-solo mode). Use TextInput plus your own suggestion list.

## Props

| Prop | Type | Default | Description | Required |
| --- | --- | --- | --- | --- |
| `label` | `string` | none | Field label; associates the trigger. | Yes |
| `options` | `ComboboxOption[]` | none | Selectable options. | Yes |
| `value` | `string` | none | Selected option value (controlled); "" shows the placeholder. | Yes |
| `onValueChange` | `(value: string) => void` | none | Called with the chosen value. | Yes |
| `id` | `string` | none | Explicit control id (wires the label); auto-generated when omitted. | No |
| `placeholder` | `string` | none | Shown when no value is selected. | No |
| `helperText` | `string` | none | Assistive text below the field. | No |
| `error` | `string` | none | Error message; renders above the helper line and sets aria-invalid. | No |
| `status` | `FieldStatus` | none | Semantic state: colours the border and styles statusMessage. `error` overrides it. | No |
| `statusMessage` | `string` | none | Message under the field for `status`, above the helper line. | No |
| `actions` | `FieldAction[]` | none | Icon buttons at the end of the label row: each runs an operation or opens a menu. | No |
| `required` | `boolean` | `false` | Marks the field required. | No |
| `disabled` | `boolean` | `false` | Disables the control. | No |
| `className` | `string` | none | Classes on the field wrapper. | No |
| `size` | `FieldSize` | `md` | Field size shared by every DS field: 40, 48 or 56px tall. | No |
| `name` | `string` | none | Submits the value with a surrounding form, through a hidden input. | No |
| `aria-describedby` | `string` | none | Extra ids the trigger is described by, after the status and helper text. | No |
| `searchable` | `boolean` | `false` | Makes the field a text input that filters the list as you type (every typed word matches the start of a word in an option's label or keywords). Use it for long lists, such as time zones or countries; the forwarded ref then points at the input. | No |

## Variants

- Available sizes: `sm`, `md`, `lg` (default `md`).

## Invalid combinations

The contract records no invalid combinations.

## Keyboard

| Key | Behaviour |
| --- | --- |
| Tab | Moves focus to and from the trigger and closes an open list. Focus moving to any element outside the field also closes it; options are never Tab stops. |
| Enter | Activates the focused control. (standard) |
| Space | Activates or toggles the focused control. (standard) |
| Escape | Closes or dismisses the open layer. (standard) |
| ArrowDown | Opens the list, then moves the highlight down to the next enabled option with wrap-around. Disabled options are skipped. |
| ArrowUp | Opens the list, then moves the highlight up to the previous enabled option with wrap-around. Disabled options are skipped. |
| Home | Opens the list if needed and moves the highlight to the first enabled option. |
| End | Opens the list if needed and moves the highlight to the last enabled option. |
| General | Printable characters (typeahead): open the list and move the highlight to the first option whose label, or any word in it, starts with what was typed; the search clears after a 0.7 s pause, and a space inside a search is part of it. |

Rows marked (standard) name a key the contract lists without a description; the behaviour shown is the usual one for that key.

## Rules

Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.

| Level | Rule | How it is checked |
| --- | --- | --- |
| MUST | ARIA: Trigger role=combobox with aria-expanded/aria-controls/aria-activedescendant. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Labelled via aria-labelledby. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Listbox with role=listbox + aria-label. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Options role=option with aria-selected. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Error sets aria-invalid + aria-describedby. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | Required stories produce no axe violations at the configured severity. | Automated by `npm run test:stories:matrix:ci`. WCAG 2.5.8. |
| MUST | The accessibility tree (role, name, state) matches the committed snapshot for every required story. | Automated by `npm run test:stories:matrix:ci`. WCAG 1.3.1, 4.1.2. |
| MUST | The component remains legible and operable under a real browser forced-colors: active pass. | Automated by `npm run test:stories:hc`. |
| MUST | Every documented keyboard interaction works as specified. | Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2. |
| MUST | A human completed an end-to-end accessibility review of this component. | Manual review, last on 2026-11-04. |
| SHOULD | Always control it: pass value and onValueChange together; there is no uncontrolled mode. | Not stated in the contract. |
| SHOULD | Prefer native Select for plain forms; Combobox earns its cost when the design needs the styled dropdown or per-option disabling. | Not stated in the contract. |
| SHOULD | Load async options into state and pass the current array; the empty state renders a localized "no matching options" row on its own. | Not stated in the contract. |
| SHOULD | Set placeholder for the empty value so the closed trigger never renders blank. | Not stated in the contract. |
| SHOULD NOT | Do not render it inside overflow-hidden containers without accepting the portal: the listbox mounts on document.body and positions itself. | Not stated in the contract. |

## Install and import

- Install: `npm install @digitaltableteur/react` (version 0.9.1, restricted access on npm).
- Import: `import { Combobox } from "@digitaltableteur/react";`
- Entry point: `@digitaltableteur/react/forms` exports it too.
- Styles: import `@digitaltableteur/react/style.css` once at the app boundary.
- Also available as the custom element `<dt-combobox>`; see the custom elements manifest.

## Links

- Machine-readable contract (Design System Contract 1.0): https://www.digitaltableteur.com/contracts/v1/Combobox.contract.json
- Markdown version of this page: https://www.digitaltableteur.com/design-system/components/combobox.md
- Component index as JSON: https://www.digitaltableteur.com/design-system/components.json
- All components: https://www.digitaltableteur.com/design-system/components
- Source: The implementation lives in a private repository. This page, its Markdown twin and the contract are generated from it at build time and are the public source of truth.
