# Combobox Field A searchable multi-select field for choosing one or more options from a long list. Section: components Docs: https://nebula.kaluza.com/components/combobox-field --- ## When to use - When the list has more than around 15 options and users need to search to narrow it down - When users need to select multiple items from a large set of options - When the list is too long to scan comfortably, making a grouped or searchable dropdown more useful than a static list ## When not to use - **Five or fewer options** - use [Radio](/components/radio) for single selection or [Checkbox](/components/checkbox) for multiple selection. Static controls are easier to scan and require less interaction. - **Single selection from a short, flat list** - use [Select Field](/components/select-field). It is simpler and more familiar for straightforward choices. - **All options are short and equally scannable** - use [Select Field](/components/select-field). Search adds friction when users can find the right option by reading the list directly. - **To indicate that a field is unavailable** - the disabled state is not supported. Use [Summary List](/components/summary-list) to show values that cannot be edited, or conditionally hide the field if it is not relevant in the current context. ## Anatomy 1. **Label** 2. **Hint text** (optional) 3. **Search icon** 4. **Selected chips** 5. **Input** 6. **Chevron** 7. **Dropdown** 8. **Option group** (optional) 9. **Option** 10. **Error text** (optional) ## Basic usage ```tsx ``` ## Variants and features ### Option groups Options can be grouped under collapsible headings by nesting an `options` array inside a parent option object. Groups are collapsed by default and expand on click or keyboard activation. When the user types a search query, all matching options become visible regardless of which groups are expanded. Avoid groups that contain only one option. Groups are only worthwhile when they help users orient themselves within a large list. Give groups concise, specific names rather than catch-all labels like "Other". ```tsx ``` ### Controlled and uncontrolled ComboboxField can be used as either a controlled or uncontrolled component. Pass `value` and `onValueChange` together for controlled usage. Use `defaultValue` alone for uncontrolled usage where you only need to read the final selection on submit. Controlled, with `value` and `onValueChange`: ```tsx const Example = () => { const [fruit, setFruit] = useState('banana'); return (

Current value: {fruit || 'none'}

); } ``` Uncontrolled, with `defaultValue`: ```tsx ``` ### Common form features ComboboxField shares validation, hint text, required/optional marking, and field width behaviour with other form fields. See the [forms pattern guide](/patterns/forms) for guidance on these topics. ## Rules and guidance ### Usage - **ComboboxField** MUST always set the multiple prop to true. Rationale: The component is designed for multi-select only. The prop is required in the type signature. [id: comboboxfield.multiple-required; testability: auto] - **ComboboxField** MUST always receive a non-empty options array. Rationale: An empty options list renders a dropdown with no choices, giving users no way to make a selection. [id: comboboxfield.options-required; testability: auto] - **ComboboxField** MUST provide both value and onValueChange when used as a controlled component. Rationale: Providing value without onValueChange creates a read-only field with no way to update selection. [id: comboboxfield.controlled-pair; testability: auto] - **ComboboxField** MUST NOT be passed the disabled prop. Rationale: The prop passes through to the native input with no Nebula styling and removes the field from the tab order, making it invisible to keyboard users with no accessible indication of why. [id: comboboxfield.no-disabled; testability: auto] - **ComboboxField** SHOULD only be used when the option list has more than around 15 items, or when multiple selection is needed. Rationale: For shorter lists, Select Field or Radio/Checkbox are simpler and easier to use. [id: comboboxfield.large-option-sets; testability: context] - **ComboboxField** SHOULD be replaced with plain text. For example in a SummaryList. When displaying a value the user cannot edit. Rationale: A field that looks interactive but cannot be changed is confusing. Plain text communicates the value without implying editability. [id: comboboxfield.non-editable-as-text; testability: manual] - **ComboboxField** SHOULD NOT be used when the option list has five or fewer items. Rationale: Static controls such as Radio and Checkbox are faster to scan and require less interaction for short lists. [id: comboboxfield.not-for-small-sets; testability: context] ### Placement - **Dropdown** WILL open below the input by default, and switch to above when there is insufficient space below. [id: comboboxfield.dropdown-placement; testability: auto] ### Content - **Option label** MUST use sentence case. [id: comboboxfield.option.label-sentence-case; testability: auto] - **Option value** MUST be unique across the entire options list, including across groups. Rationale: Duplicate values cause incorrect selection state. [id: comboboxfield.option.unique-values; testability: auto] - **Option group label** MUST use sentence case. [id: comboboxfield.group.sentence-case; testability: auto] - **Option label** MUST NOT repeat the group name in the option label. Rationale: The group header already provides this context, so repeating it is redundant. [id: comboboxfield.option.label-no-group-repeat; testability: manual] - **Option label** SHOULD be five words or fewer. Rationale: Long option labels are harder to scan and may be truncated inside chips. [id: comboboxfield.option.label-concise; testability: auto] - **Option group label** SHOULD NOT use generic names such as "Other" or "Miscellaneous". Rationale: Generic group names do not help users find the option they are looking for. [id: comboboxfield.group.specific-names; testability: manual] ### Interaction - **ComboboxField** WILL receive focus on the search input when Tab is pressed. [id: comboboxfield.keyboard.tab-focuses-input; testability: auto] - **ComboboxField** WILL open the dropdown when ArrowDown is pressed from the input. [id: comboboxfield.keyboard.arrow-down-opens; testability: auto] - **ComboboxField** WILL move focus into the dropdown option list when ArrowDown is pressed while the dropdown is already open. [id: comboboxfield.keyboard.arrow-down-navigates; testability: auto] - **ComboboxField** WILL close the dropdown when Escape is pressed. [id: comboboxfield.keyboard.escape-closes; testability: auto] - **ComboboxField** WILL remove the last selected chip when Backspace is pressed in an empty input. [id: comboboxfield.keyboard.backspace-removes; testability: auto] - **ComboboxField** WILL move focus to the last selected chip when ArrowLeft is pressed at the start of the input. [id: comboboxfield.keyboard.arrow-left-chips; testability: auto] - **Selected chip** WILL be removable with Backspace, Delete, or Enter when focused. [id: comboboxfield.keyboard.chip-remove; testability: auto] - **Selected chips** WILL be navigable using ArrowLeft and ArrowRight, returning focus to the input from the last chip. [id: comboboxfield.keyboard.chip-arrow-nav; testability: auto] - **Dropdown options** WILL be navigable using ArrowUp and ArrowDown, with ArrowUp from the first option returning focus to the input. [id: comboboxfield.keyboard.dropdown-arrow-nav; testability: auto] - **Dropdown options** WILL jump to the first option on Home and the last option on End. [id: comboboxfield.keyboard.dropdown-home-end; testability: auto] - **ComboboxField** WILL redirect printable character key presses from the dropdown to the search input, appending the typed character. [id: comboboxfield.keyboard.type-to-search; testability: auto] - **ComboboxField** WILL display a "No results found" message when the search query matches no options. Rationale: An empty dropdown with no explanation is confusing and looks like a loading failure. [id: comboboxfield.no-results-message; testability: auto] - **ComboboxField** WILL announce the number of available results to screen readers when the dropdown opens or the option count changes. [id: comboboxfield.live-region; testability: auto] ### Grouping - **Option group** SHOULD contain at least two options. Rationale: A group with a single option adds a heading without the benefit of grouping. [id: comboboxfield.group.min-two-options; testability: auto] ### Composition - **ComboboxField** WILL render a hidden input for each selected value when the name prop is provided, to support native form submission. [id: comboboxfield.hidden-inputs; testability: auto] - **Selected chip** WILL truncate long option labels within the input wrapper rather than overflowing past the chevron. [id: comboboxfield.chip-truncation; testability: auto] ## Accessibility ### In brief Combobox Field is a text input with the role `combobox` that opens a list of options, single or multiple, which can be nested in groups. Moving into the list moves real focus onto the options, so its keyboard model is Nebula's own and not the browser's. ### What Nebula provides - A real `label` linked to the input, `aria-required` unless the field is `optional`, and an `error` linked with `aria-invalid` and `aria-describedby`. - The input has `aria-expanded`, `aria-haspopup="listbox"`, `aria-controls` and `aria-autocomplete="list"`. Options have `aria-selected`, and a multiple list is marked multiselectable. - A live region announces how many results are available as the list opens and is filtered, or that there are none. - Each chip's remove button is named with the option label followed by "press to remove" (see [Chip](/components/chip/?tab=accessibility)). ### Keyboard behaviour The input is the only tab stop; the list, the options and the chips are reached with the arrow keys. `Arrow Down` opens the list and then moves focus into it, `Arrow Up`, `Arrow Down`, `Home` and `End` move between options, `Enter` selects an option or opens or closes a group, and `Esc` closes the list. In a multiple field, `Arrow Left` at the start of the input moves to the chips, `Backspace` there removes the last chip, `Arrow Left` and `Arrow Right` move between chips and back to the input, and `Backspace`, `Delete` or `Enter` on a chip removes it. #### Interactions - **Tab** - Moves focus to the input ### Accessible design #### Never rely on the placeholder to name the field The field shows the fixed prompt "Type to search or select from list" while nothing is selected, and it is shown at half opacity. It is a prompt, not a label: write a label that names what the user is choosing. WCAG: [3.3.2 Labels or Instructions](https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html) #### Write group names that make sense on their own A group header is announced as an option, by its label alone, so the name has to say what is inside the group without the options beside it. WCAG: [2.4.6 Headings and Labels](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html) ### Accessible development #### Always pass errors through `error`, or through a `Form` with the field `name` This is what sets `aria-invalid` and links the message to the input. Text placed beside the field is not announced with it. WCAG: [3.3.1 Error Identification](https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html) #### If a selection changes content elsewhere on the page, announce the change The component only announces how many results are available. Tell the user, through a status message, when choosing an option updates a list, a total or a form section, because focus stays in the field. WCAG: [4.1.3 Status Messages](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html) ### Accessibility testing status Reviewed on 9 October 2026 against v17.0.1. | Test | Status | Tested with | | --- | --- | --- | | Default state | Tested | axe-core via the Storybook test runner (CI) | | Advanced states | Partially tested | axe-core via the Storybook test runner (CI) | | Keyboard navigation | Partially tested | keyboard (reported by the owner) | | Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) | What each test covers: - Default state: The component as it first appears, before anything is clicked or changed. - Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering. - Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring. - Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state. What these statuses mean: - Tested: Every check in this row passed in an automated run that repeats on every pull request. - Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change. - Partially tested: Some checks in this row were run once and passed; the rest are still to do. - Issue found: A check in this row failed. The ticket is shown beside the status. - Not tested: No check in this row has been run yet. - Not applicable: This row does not apply to the component, for the reason given beside it. The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used. ## Properties ### ComboboxField - `id` (type `string`, required). Unique id for the input, used to link the label, hint and error to it. Also used as the base for related ids such as `{id}-error`. - `error` (type `ReactNode`, optional). Error message shown below the input. When set, the input gets `aria-invalid` and an error border, and the message is linked to it with `aria-describedby`. Takes precedence over an error provided by a surrounding `Form`. - `label` (type `string`, required). Visible label for the field. It is associated with the input, so it forms the input's accessible name. - `name` (type `string`, optional). The `name` attribute passed to the input. Also used to look up the field's error from a surrounding `Form`. Name for the combobox. In `ComboboxField` it is also used to look up the field error from a surrounding `Form`. - `hint` (type `ReactNode`, optional). Helper text shown below the label. It is part of the label element, so it is read as part of the input's accessible name. - `optional` (type `boolean`, optional, default `false`). Marks the field as not required. This appends "(optional)" to the label (or, when the theme marks required fields, hides the required asterisk) and sets `aria-required` on the input only when the field is not optional. - `fullWidth` (type `'always' | 'never' | 'small'`, optional, default `'small'`). Controls when the field stretches to the full width of its container. `always` is always full width, `never` always uses the width set by `characters`, and `small` is full width on small screens only. - `characters` (type `number`, optional, default `25`). Width of the input, as an approximate number of characters (the width is set to `0.8em` per character, rounded up). Does not limit how much can be typed. - `affixWidth` (type `number`, optional, default `0`). Extra width in pixels added to the width calculated from `characters`, to make room for icons or buttons inside the input. - `options` (type `Option[]`, required). Options to choose from, each with a `label` and `value`. An option can contain its own `options` to form a collapsible group. Typing in the input filters options by label. - `multiple` (type `true | false`, optional, default `false`). Allows more than one option to be selected. Selected options are shown as removable chips, and `value`, `defaultValue` and `onValueChange` use arrays of option values. - `value` (type `string[] | string | null`, optional). Controlled selected option values. Use together with `onValueChange`. - `defaultValue` (type `string[] | string`, optional). Initially selected option values when uncontrolled. Values that do not match an option are ignored. - `onValueChange` (type `(newValue: string[]) => void | (newValue: string | null) => void`, optional). Called with the array of selected option values whenever the selection changes.