FairGarden Design

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.

Species
Birds Seen
Habitat
Tags (optional)
Leader
Choose a leader from the list.
Reviewer
ComboboxStates.tsx
'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.

Primary Plum
Primary Slate
ComboboxColor.tsx
'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

Parks Visited

forest

Parks Visited
ComboboxGrounds.tsx
'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.

PropTypeDescription
aria-label
string | undefined

Names the input when no visible label exists. Prefer a FieldLabel.

defaultValue
ComboboxOption[] | ComboboxOption | null | undefined

The initial choice, uncontrolled.

value
ComboboxOption[] | ComboboxOption | null | undefined

The chosen option (or options), controlled.

onValueChange
| ((
    value: ComboboxOption[] | ComboboxOption | null,
    eventDetails: ComboboxRoot.ChangeEventDetails,
  ) => void)
| undefined
clearLabel
string | undefined

The clear בs accessible name. Default “Clear”.

createText
((query: string) => React.ReactNode) | undefined

The add row’s words. Default “Add ‘[query]'".

emptyText
((query: string) => React.ReactNode) | undefined

The empty row. Default “No matches for ‘[query]'". Keep what the user typed.

error
React.ReactNode | undefined

A fetch failure, shown with the danger glyph instead of silently closing: “Couldn’t load suggestions. Keep typing or try again.”

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'
| undefined

A leading icon in the box, such as search (inline tier).

items
ComboboxItems | undefined

The options, or titled groups. Base UI filters them as the user types.

loading
boolean | undefined

Shows the loading row (“Searching…") while suggestions load.

loadingText
React.ReactNode | undefined

The loading row’s words. Default “Searching…".

multiple
boolean | undefined

true chooses several values, shown as removable chips in the box. Default false.

onCreate
((query: string) => void) | undefined

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.

placeholder
string | undefined

The input’s placeholder, ending in “…"; never the label.

plate
boolean | undefined

The box face becomes a nested white scope; patterned grounds only (§10.1).

primary
| 'olive'
| 'sage'
| 'slate'
| 'sand'
| 'gray'
| 'mauve'
| 'brown'
| 'bronze'
| 'gold'
| 'red'
| 'ruby'
| 'crimson'
| 'tomato'
| 'pink'
| 'plum'
| 'indigo'
| 'iris'
| 'violet'
| 'purple'
| null
| undefined

Primary Radix scale: edge, value, icons, chips and focus ring. Never defaulted [D133].

removeLabel
((label: string) => string) | undefined

A chip בs accessible name. Default “Remove [label]".

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
| undefined

Secondary Radix scale. Unused at rest; the danger scale while invalid.

triggerLabel
string | undefined

The chevron’s accessible name. Default “Show options”.

className
string | undefined

Class names for the box, added after the module’s own.

combobox
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).