Combobox
A value chosen from a filtered list, such as countries, species or members, with optional multiple selection shown as chips: Base UI's Combobox, inside a Field. Under about 15 options, use Select.
import { Combobox } from '@fairgarden-private/design/components/Combobox'
Options are { value, label } objects, or titled groups. The box is the field box, with the ring on any focus; focus stays in the input while the arrow keys highlight rows. Label it with <FieldLabel nativeLabel={false}>. When nothing matches, the empty row keeps what the user typed ("No matches for 'egrit'") instead of closing; while suggestions load it reads "Searching…"; a fetch failure shows the danger glyph and a message.
Single, multiple and states
Single, multiple and states
The chosen item's text fills the input and a ✓ marks it in the list. multiple turns choices into removable chips inside the box, which grows and never scrolls sideways; Backspace in an empty input focuses the last chip and a second Backspace removes it. onCreate adds the "+ Add '…'" row when nothing matches exactly. Matches are marked by --font-weight-7, never color.
'use client'
import * as React from 'react'
import {
Combobox,
type ComboboxOption,
} from '@fairgarden-private/design/components/Combobox'
import { Field, FieldError, FieldLabel } from '@fairgarden-private/design/components/Field'
import styles from './states.module.css'
const species: ComboboxOption[] = [
{ value: 'heron', label: 'Great Blue Heron' },
{ value: 'egret', label: 'Great Egret' },
{ value: 'bittern', label: 'American Bittern' },
{ value: 'ibis', label: 'Glossy Ibis' },
{ value: 'rail', label: 'Clapper Rail' },
{ value: 'stilt', label: 'Black-necked Stilt' },
{ value: 'avocet', label: 'American Avocet' },
{ value: 'plover', label: 'Piping Plover' },
{ value: 'tern', label: 'Least Tern' },
{ value: 'skimmer', label: 'Black Skimmer' },
{ value: 'osprey', label: 'Osprey' },
{ value: 'kingfisher', label: 'Belted Kingfisher' },
{ value: 'grebe', label: 'Pied-billed Grebe' },
{ value: 'coot', label: 'American Coot' },
{ value: 'gallinule', label: 'Common Gallinule' },
{ value: 'sora', label: 'Sora' },
]
const habitats = [
{
label: 'Wetland',
items: [
{ value: 'marsh', label: 'Salt Marsh' },
{ value: 'swamp', label: 'Cypress Swamp' },
],
},
{
label: 'Upland',
items: [
{ value: 'prairie', label: 'Tallgrass Prairie' },
{ value: 'oak', label: 'Oak Savanna' },
],
},
]
/**
* Single, multiple (chips), grouped and creatable comboboxes, then invalid
* and disabled ones. Matches in the list are marked by weight.
*/
export function ComboboxStates() {
const [tags, setTags] = React.useState<ComboboxOption[]>([
{ value: 'dawn', label: 'Dawn Walk' },
{ value: 'family', label: 'Family' },
])
const [chosenTags, setChosenTags] = React.useState<ComboboxOption[]>([])
return (
<div className={styles.stack}>
<Field>
<FieldLabel nativeLabel={false}>Species</FieldLabel>
<Combobox items={species} placeholder="Start typing a name…" />
</Field>
<Field>
<FieldLabel nativeLabel={false}>Birds Seen</FieldLabel>
<Combobox
multiple
items={species}
defaultValue={[species[0], species[1]]}
placeholder="Add a bird…"
/>
</Field>
<Field>
<FieldLabel nativeLabel={false}>Habitat</FieldLabel>
<Combobox items={habitats} icon="search" placeholder="Search habitats…" />
</Field>
<Field>
<FieldLabel nativeLabel={false} optional>
Tags
</FieldLabel>
<Combobox
multiple
items={tags}
value={chosenTags}
onValueChange={setChosenTags}
onCreate={(query) => {
const option = { value: query.toLowerCase(), label: query }
setTags((current) => [...current, option])
setChosenTags((current) => [...current, option])
}}
placeholder="Add a tag…"
/>
</Field>
<Field invalid>
<FieldLabel nativeLabel={false}>Leader</FieldLabel>
<Combobox items={species} placeholder="Choose a leader…" />
<FieldError match>Choose a leader from the list.</FieldError>
</Field>
<Field disabled>
<FieldLabel nativeLabel={false}>Reviewer</FieldLabel>
<Combobox items={species} defaultValue={species[10]} />
</Field>
</div>
)
}
Primary and secondary
Primary and secondary
primary sets the box, value, icons and chips; secondary is unused at rest and becomes the danger scale while invalid.
'use client'
import {
Combobox,
type ComboboxOption,
} from '@fairgarden-private/design/components/Combobox'
import { Field, FieldLabel } from '@fairgarden-private/design/components/Field'
import styles from './color.module.css'
const towns: ComboboxOption[] = [
{ value: 'ashby', label: 'Ashby' },
{ value: 'bexley', label: 'Bexley' },
{ value: 'carrow', label: 'Carrow' },
{ value: 'dunmore', label: 'Dunmore' },
]
/** `primary` recolors the box and the chips inside it; the popup keeps the white scope's defaults. */
export function ComboboxColor() {
return (
<div className={styles.stack}>
<Field>
<FieldLabel nativeLabel={false}>Primary Plum</FieldLabel>
<Combobox primary="plum" multiple items={towns} defaultValue={[towns[1]]} />
</Field>
<Field>
<FieldLabel nativeLabel={false}>Primary Slate</FieldLabel>
<Combobox primary="slate" items={towns} placeholder="Choose a town…" />
</Field>
</div>
)
}
On grounds
On paper and forest
The box and its chips follow their ground; the popup renders in a portal as the white scope.
paper
forest
'use client'
import {
Combobox,
type ComboboxOption,
} from '@fairgarden-private/design/components/Combobox'
import { Field, FieldLabel } from '@fairgarden-private/design/components/Field'
import { PresetGround } from '@/components/PresetGround'
import styles from './grounds.module.css'
const parks: ComboboxOption[] = [
{ value: 'acadia', label: 'Acadia' },
{ value: 'everglades', label: 'Everglades' },
{ value: 'glacier', label: 'Glacier' },
{ value: 'olympic', label: 'Olympic' },
]
const presets = ['paper', 'forest'] as const
/** The box and its chips follow the ground; the popup is always the white scope. */
export function ComboboxGrounds() {
return (
<div className={styles.row}>
{presets.map((preset) => (
<PresetGround key={preset} preset={preset} className={styles.face}>
<p className={styles.name}>{preset}</p>
<Field>
<FieldLabel nativeLabel={false}>Parks Visited</FieldLabel>
<Combobox multiple items={parks} defaultValue={[parks[0]]} placeholder="Add a park…" />
</Field>
</PresetGround>
))}
</div>
)
}
API Reference
A Base UI Combobox inside a Field (label it with
<FieldLabel nativeLabel={false}>). Focus stays in the input while the
arrow keys highlight rows; Backspace in an empty input focuses the last
chip, and a second Backspace removes it.
| Prop | Type | Description |
|---|---|---|
aria-label | | Names the input when no visible label exists. Prefer a |
defaultValue | | The initial choice, uncontrolled. |
value | | The chosen option (or options), controlled. |
onValueChange | | |
clearLabel | | The clear בs accessible name. Default “Clear”. |
createText | | The add row’s words. Default “Add ‘[query]'". |
emptyText | | The empty row. Default “No matches for ‘[query]'". Keep what the user typed. |
error | | A fetch failure, shown with the danger glyph instead of silently closing: “Couldn’t load suggestions. Keep typing or try again.” |
icon | | A leading icon in the box, such as |
items | | The options, or titled groups. Base UI filters them as the user types. |
loading | | Shows the loading row (“Searching…") while suggestions load. |
loadingText | | The loading row’s words. Default “Searching…". |
multiple | |
|
onCreate | | Adds the creatable row “+ Add ‘[query]'” when nothing matches exactly
(flat lists only). Called with the query; add the option to |
placeholder | | The input’s placeholder, ending in “…"; never the label. |
plate | | The box face becomes a nested |
primary | | Primary Radix scale: edge, value, icons, chips and focus ring. Never defaulted [D133]. |
removeLabel | | A chip בs accessible name. Default “Remove [label]". |
secondary | | Secondary Radix scale. Unused at rest; the danger scale while invalid. |
triggerLabel | | The chevron’s accessible name. Default “Show options”. |
className | | Class names for the box, added after the module’s own. |
ComboboxOption
One option: a string value and the words the list and the input show.
type ComboboxOption = { value: string; label: string; disabled?: boolean }ComboboxOptionGroup
A titled group of options.
type ComboboxOptionGroup = {
/** The group label, in `type-label` caps. */
label: string;
items: ComboboxOption[];
}ComboboxProps
Props for Combobox: Base UI Combobox.Root props plus the options, rows and color axes.
type ComboboxProps<Multiple extends boolean | undefined = false> = {
/** The options, or titled groups. Base UI filters them as the user types. */
items: ComboboxItems;
/** `true` chooses several values, shown as removable chips in the box. Default `false`. */
multiple?: boolean | undefined;
/** The chosen option (or options), controlled. */
value?: ComboboxOption[] | ComboboxOption | null;
/** The initial choice, uncontrolled. */
defaultValue?: ComboboxOption[] | ComboboxOption | null;
onValueChange?: (
value: ComboboxOption[] | ComboboxOption | null,
eventDetails: ComboboxRoot.ChangeEventDetails,
) => void;
/** The input's placeholder, ending in "…"; never the label. */
placeholder?: string;
/** A leading icon in the box, such as `search` (inline tier). */
icon?:
| 'search'
| 'arrow_forward'
| 'arrow_upward'
| 'expand_more'
| 'close'
| 'remove'
| 'add'
| 'check'
| 'circle'
| 'chevron_right'
| 'chevron_left'
| 'menu'
| 'more_horiz'
| 'play_arrow'
| 'pause'
| 'download'
| 'zoom_in'
| 'zoom_out'
| 'recenter'
| 'help'
| 'mail';
/** The empty row. Default "No matches for '[query]'". Keep what the user typed. */
emptyText?: (query: string) => React.ReactNode;
/** Shows the loading row ("Searching…") while suggestions load. */
loading?: boolean;
/** The loading row's words. Default "Searching…". */
loadingText?: React.ReactNode;
/**
* A fetch failure, shown with the danger glyph instead of silently
* closing: "Couldn't load suggestions. Keep typing or try again."
*/
error?: React.ReactNode;
/**
* Adds the creatable row "+ Add '[query]'" when nothing matches exactly
* (flat lists only). Called with the query; add the option to `items`
* and select it through `value`.
*/
onCreate?: (query: string) => void;
/** The add row's words. Default "Add '[query]'". */
createText?: (query: string) => React.ReactNode;
/** The clear ×'s accessible name. Default "Clear". */
clearLabel?: string;
/** The chevron's accessible name. Default "Show options". */
triggerLabel?: string;
/** A chip ×'s accessible name. Default "Remove [label]". */
removeLabel?: (label: string) => string;
/** Names the input when no visible label exists. Prefer a `FieldLabel`. */
'aria-label'?: string;
/** Class names for the box, added after the module's own. */
className?: string;
/** The box face becomes a nested `white` scope; patterned grounds only (§10.1). */
plate?: boolean;
/** Primary Radix scale: edge, value, icons, chips and focus ring. Never defaulted [D133]. */
primary?:
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null;
/** Secondary Radix scale. Unused at rest; the danger scale while invalid. */
secondary?:
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| 'amber'
| 'blue'
| 'cyan'
| 'grass'
| 'green'
| 'jade'
| 'lime'
| 'mint'
| 'orange'
| 'sky'
| 'teal'
| 'yellow'
| null;
}Specification: DESIGN-SYSTEM.md §10.6 (combobox and autocomplete) and §10.11 (the removable chip).