Open UI

ComboBox

A type-ahead select that filters options as the user types. For non-filterable dropdowns, use Select; for multi-select, use TagField.

import { ComboBox, ComboBoxItem } from "@opengovsg/oui"export const Example = () => {  return (    <ComboBox label="Favourite animal">      <ComboBoxItem>Aardvark</ComboBoxItem>      <ComboBoxItem>Cat</ComboBoxItem>      <ComboBoxItem>Dog</ComboBoxItem>      <ComboBoxItem>Kangaroo</ComboBoxItem>      <ComboBoxItem>Panda</ComboBoxItem>      <ComboBoxItem>Snake</ComboBoxItem>    </ComboBox>  )}

Usage

Use ComboBox when users need to filter a list by typing. Use Select for a non-filterable dropdown. Use TagField for multi-select with filtering. Use SearchField for free-text search that's not constrained to a list.

import { ComboBox, ComboBoxEmptyState, ComboBoxItem } from "@opengovsg/oui"
<ComboBox label="Favourite animal">
  <ComboBoxItem>Aardvark</ComboBoxItem>
  <ComboBoxItem>Cat</ComboBoxItem>
  <ComboBoxItem>Dog</ComboBoxItem>
</ComboBox>

Alternatively, install the component as local source via the shadcn CLI:

npx shadcn@latest add https://oui.open.gov.sg/r/combo-box.json
pnpm dlx shadcn@latest add https://oui.open.gov.sg/r/combo-box.json
npx shadcn@latest add https://oui.open.gov.sg/r/combo-box.json
bunx --bun shadcn@latest add https://oui.open.gov.sg/r/combo-box.json

ComboBox provides a text input that filters a list of options as the user types, built on React Aria's ComboBox. It handles keyboard navigation, focus management, and ARIA semantics automatically.

If the combo-box does not have a visible label, an aria-label or aria-labelledby prop must be passed instead to identify it to assistive technology.

Anatomy

ComboBox consists of:

  • ComboBox — the trigger input + listbox container; manages state
    • ComboBoxItem — one option in the dropdown list
    • ComboBoxEmptyState — rendered when the typed filter matches no items

Examples

Sizes

Use the size prop to change the size of the combo-box. Defaults to "md".

import { ComboBox, ComboBoxItem } from "@opengovsg/oui"export const Example = () => {  return (    <div className="flex flex-col gap-6">      <ComboBox size="xs" label="Extra Small">        <ComboBoxItem>Aardvark</ComboBoxItem>        <ComboBoxItem>Cat</ComboBoxItem>        <ComboBoxItem>Dog</ComboBoxItem>      </ComboBox>      <ComboBox size="sm" label="Small">        <ComboBoxItem>Aardvark</ComboBoxItem>        <ComboBoxItem>Cat</ComboBoxItem>        <ComboBoxItem>Dog</ComboBoxItem>      </ComboBox>      <ComboBox size="md" label="Medium (default)">        <ComboBoxItem>Aardvark</ComboBoxItem>        <ComboBoxItem>Cat</ComboBoxItem>        <ComboBoxItem>Dog</ComboBoxItem>      </ComboBox>    </div>  )}

With Description and Text Slots

Use description on ComboBox for helper text. Use the description prop on ComboBoxItem for per-item context.

import { ComboBox, ComboBoxItem } from "@opengovsg/oui"export const Example = () => {  return (    <ComboBox label="Select action">      <ComboBoxItem description="Add to current watch queue.">        Add to queue      </ComboBoxItem>      <ComboBoxItem description="Post a review for the episode.">        Add review      </ComboBoxItem>      <ComboBoxItem        description="Add series to your subscription list and be notified when a new          episode airs."      >        Subscribe to series      </ComboBoxItem>      <ComboBoxItem description="Report an issue/violation.">        Report      </ComboBoxItem>    </ComboBox>  )}

Controlled

Use inputValue / onInputChange to control the text input and selectedKey / onSelectionChange to control the selected item programmatically.

import type { Key } from "react-aria-components"import { useState } from "react"import { ComboBox, ComboBoxItem } from "@opengovsg/oui"const animals = [  { id: "1", textValue: "Aardvark" },  { id: "2", textValue: "Cat" },  { id: "3", textValue: "Dog" },  { id: "4", textValue: "Kangaroo" },  { id: "5", textValue: "Panda" },  { id: "6", textValue: "Snake" },]export const Example = () => {  const [inputValue, setInputValue] = useState("")  const [selectedKey, setSelectedKey] = useState<Key | null>(null)  return (    <div className="flex flex-col gap-2">      <ComboBox        label="Favourite animal"        items={animals}        inputValue={inputValue}        onInputChange={setInputValue}        selectedKey={selectedKey}        onSelectionChange={setSelectedKey}      >        {(item) => <ComboBoxItem id={item.id}>{item.textValue}</ComboBoxItem>}      </ComboBox>      <p className="text-sm">        Selected key: <code>{selectedKey ?? "none"}</code>      </p>      <p className="text-sm">        Input value: <code>{inputValue || "(empty)"}</code>      </p>    </div>  )}

Validation

Use isRequired and isInvalid + errorMessage for form validation.

import { ComboBox, ComboBoxItem } from "@opengovsg/oui"export const Example = () => {  return (    <div className="flex flex-col gap-6">      <ComboBox label="Required field" isRequired>        <ComboBoxItem>Aardvark</ComboBoxItem>        <ComboBoxItem>Cat</ComboBoxItem>        <ComboBoxItem>Dog</ComboBoxItem>      </ComboBox>      <ComboBox        label="Realtime invalid"        isInvalid        errorMessage="Please select a valid option"      >        <ComboBoxItem>Aardvark</ComboBoxItem>        <ComboBoxItem>Cat</ComboBoxItem>        <ComboBoxItem>Dog</ComboBoxItem>      </ComboBox>    </div>  )}

Custom Empty State

Use renderEmptyState to provide a custom message when no items match. The built-in ComboBoxEmptyState helper renders the localised "No matching results" string and accepts an optional className.

import { ComboBox, ComboBoxEmptyState, ComboBoxItem } from "@opengovsg/oui"export const Example = () => {  return (    <ComboBox      label="Favourite animal"      renderEmptyState={() => <ComboBoxEmptyState className="text-red-500" />}    >      <ComboBoxItem>Aardvark</ComboBoxItem>      <ComboBoxItem>Cat</ComboBoxItem>      <ComboBoxItem>Dog</ComboBoxItem>    </ComboBox>  )}

Content

ComboBox follows the Collection Components API, accepting both static and dynamic collections.

Static collections pass children directly when the full list is known up front:

<ComboBox label="Animal">
  <ComboBoxItem>Aardvark</ComboBoxItem>
  <ComboBoxItem>Cat</ComboBoxItem>
</ComboBox>

Dynamic collections pass an iterable to defaultItems (uncontrolled) or items (controlled), and a render function as children. Each item object must have an id property (or provide a unique id prop on the rendered ComboBoxItem).

import type { Key } from "react-aria-components"import { useState } from "react"import { ComboBox, ComboBoxItem } from "@opengovsg/oui"export const Example = () => {  const options = [    { id: 1, textValue: "Aerospace" },    { id: 2, textValue: "Mechanical" },    { id: 3, textValue: "Civil" },    { id: 4, textValue: "Biomedical" },    { id: 5, textValue: "Nuclear" },    { id: 6, textValue: "Industrial" },    { id: 7, textValue: "Chemical" },    { id: 8, textValue: "Agricultural" },    { id: 9, textValue: "Electrical" },  ]  const [selectedId, setSelectedId] = useState<Key | null>(null)  return (    <div className="flex flex-col">      <ComboBox        label="Engineering"        defaultItems={options}        selectedKey={selectedId}        onSelectionChange={setSelectedId}      >        {(item) => <ComboBoxItem>{item.textValue}</ComboBoxItem>}      </ComboBox>      <p>Selected id: {selectedId ?? "None"}</p>    </div>  )}

Each item accepts an id prop which is passed to onSelectionChange. If the item objects have an id field, it is used automatically. Use textValue on the item object (or ComboBoxItem's textValue prop) when the item's string value cannot be derived from its children — for example, when children is a render function or contains non-string nodes.

Validation

See the Forms guide for end-to-end patterns including React Hook Form + Zod.

  • isRequired — marks the field as required; validation fires on form submit (native) or immediately (aria).
  • validate — a custom function (value: ComboBoxValidationValue) => ValidationError | true | null | undefined. Return a string to show as the error, or null/true for valid.
  • isInvalid + errorMessage — use together for realtime validation feedback without waiting for a form submit. errorMessage accepts a ReactNode or a function (validation: ValidationResult) => string.
  • validationBehavior"native" (default) blocks form submission when invalid; "aria" only marks the field via ARIA attributes and allows submission — required for React Hook Form.

Events

  • onInputChange — Called when the text input value changes, receiving the new string.
  • onSelectionChange — Called when the user selects an item, receiving the item's Key (or null when cleared).
  • onOpenChange — Called when the listbox opens or closes, receiving (isOpen: boolean, menuTrigger?: MenuTriggerAction).
  • onClear — Called when the clear button is pressed. Providing this prop renders a clear button next to the expand button; requires the component to be fully controlled (inputValue, onInputChange, selectedKey, onSelectionChange).
  • onFocus / onBlur — Called when the input receives or loses focus.
  • onFocusChange — Called when the focus state changes, receiving a boolean.
  • onKeyDown / onKeyUp — Called on keyboard events within the input.

Accessibility

  • The combo-box is built on native <input> with full ARIA combobox/listbox/option semantics managed by React Aria.
  • The label, description, and error message are automatically wired via aria-labelledby and aria-describedby.
  • If no visible label is provided, pass aria-label or aria-labelledby to the ComboBox.
  • Keyboard interaction: type to filter options; Up/Down arrows to navigate the list; Enter to select; Escape to close; Alt+Down to open without filtering.
  • Focus returns to the input after an item is selected or the listbox is dismissed.

Slots

Slots are named regions of the component you can target with custom Tailwind classes via the classNames prop. Each slot below corresponds to a key on the classNames object.

ComboBox slots:

  • base: The root flex-column container wrapping the entire field (label, input group, description, error).
  • label: The label element rendered above the input.
  • group: The input + expand-button wrapper (the visible bordered container).
  • field: The <input> element itself.
  • expandButton: The chevron button that opens and closes the listbox.
  • icon: The chevron/icon rendered inside the expand button (and clear button).
  • popover: The floating popover that contains the listbox.
  • list: The scrollable <listbox> element inside the popover.
  • clearButton: The clear button rendered when onClear is provided.
  • emptyState: The wrapper for the empty-state message (applied to ComboBoxEmptyState).

ComboBoxItem slots (via ComboBoxItem's classNames):

  • container: The list item row.
  • label: The primary text label inside the item.
  • description: The secondary description text inside the item.

Custom Styles

import { ComboBox, ComboBoxItem } from "@opengovsg/oui"export const Example = () => {  return (    <ComboBox      label="Favourite animal"      classNames={{        label: "text-purple-600 font-semibold",        group: "border-purple-400 focus-within:border-purple-600",      }}    >      <ComboBoxItem>Aardvark</ComboBoxItem>      <ComboBoxItem>Cat</ComboBoxItem>      <ComboBoxItem>Dog</ComboBoxItem>      <ComboBoxItem>Kangaroo</ComboBoxItem>    </ComboBox>  )}

Props

ComboBox

PropTypeDefaultDescription
childrenReactNode | ((item: T) => ReactNode)-Static children or render function for dynamic items
labelReactNode-The visible label for the field
itemsT[]-The list of items (controlled dynamic collection)
defaultItemsT[]-The default list of items (uncontrolled dynamic collection)
inputValuestring-The current text input value (controlled)
defaultInputValuestring-The default text input value (uncontrolled)
onInputChange(value: string) => void-Called when the input value changes
selectedKeyKey | null-The key of the selected item (controlled)
defaultSelectedKeyKey-The default selected key (uncontrolled)
onSelectionChange(key: Key | null) => void-Called when the selection changes
isDisabledbooleanfalseWhether the field is disabled
isReadOnlybooleanfalseWhether the field is read-only
isRequiredbooleanfalseWhether the field is required
isInvalidbooleanfalseWhether the field should display as invalid
validate(value: ComboBoxValidationValue) => ValidationError | true | null | undefined-Custom validation function
errorMessageReactNode | ((validation: ValidationResult) => string)-Error message to display when invalid
validationBehavior"native" | "aria""native"Whether to use native HTML form validation or ARIA-only
size"xs" | "sm" | "md""md"The size of the component
classNamesSlotsToClasses<ComboBoxSlots & "clearButton" | "emptyState">-Custom Tailwind classes for component slots
descriptionReactNode | null-Helper text shown below the field
dependenciesany[]-Values that invalidate the item cache when using dynamic collections
listLayoutOptionsListLayoutOptions-Additional props spread to the list layout (virtualisation)
renderEmptyState(props: ListBoxRenderProps) => ReactNode-Custom renderer for the empty state when no items match
onClear() => void-If provided, renders a clear button; called when it is pressed. Requires fully controlled state.
onOpenChange(isOpen: boolean, menuTrigger?: MenuTriggerAction) => void-Called when the listbox opens or closes
onFocus(e: FocusEvent) => void-Called when the input receives focus
onBlur(e: FocusEvent) => void-Called when the input loses focus
onFocusChange(isFocused: boolean) => void-Called when the focus state changes
onKeyDown(e: KeyboardEvent) => void-Called on key down within the input
onKeyUp(e: KeyboardEvent) => void-Called on key up within the input
inputPropsPartial<InputProps>-Additional props spread to the underlying <input> element

Inherits all remaining props from React Aria's ComboBox. Notable ones: autoFocus, allowsCustomValue, menuTrigger, formValue, excludeFromTabOrder.

errorMessage render-prop argument

When errorMessage is a function, it receives a ValidationResult object:

PropertyTypeDescription
isInvalidbooleanWhether the field is currently invalid
validationErrorsstring[]List of validation error strings
validationDetailsValidityStateThe native HTML ValidityState object

renderEmptyState render-prop argument

When renderEmptyState is a function, it receives a ListBoxRenderProps object:

PropertyTypeDescription
isEmptybooleanWhether the collection has no items
isFocusedbooleanWhether the listbox is focused
isDropTargetbooleanWhether the listbox is a drag-and-drop target

ComboBoxItem

PropTypeDefaultDescription
childrenReactNode | ((renderProps: ListBoxItemRenderProps) => ReactNode)-The item content, or a render function
idKey-A unique identifier for the item; used as the value passed to onSelectionChange
textValuestring-Plain-text value for filtering and accessibility; required when children is not a string
descriptionReactNode-Secondary descriptive text shown below the item label
isDisabledbooleanfalseWhether the item is disabled
classNamesSlotsToClasses<"container" | "label" | "description">-Custom Tailwind classes for item slots

Inherits all remaining props from React Aria's ListBoxItem.

ComboBoxEmptyState

PropTypeDefaultDescription
size"xs" | "sm" | "md"-Size variant; inherits from the parent ComboBox automatically
classNamestring-Additional Tailwind classes applied to the empty-state container

ComboBoxFuzzy (deprecated)

ComboBoxFuzzy is a deprecated controlled variant that provided built-in fuzzy search with match highlighting. Use ComboBox instead and supply your own filtering logic via items + onInputChange. The only notable additional prop it exposed was itemClassNames, which accepts SlotsToClasses<"container" | "label" | "description" | "highlight"> to style the highlight span inside matched items.

On this page