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.jsonpnpm dlx shadcn@latest add https://oui.open.gov.sg/r/combo-box.jsonnpx shadcn@latest add https://oui.open.gov.sg/r/combo-box.jsonbunx --bun shadcn@latest add https://oui.open.gov.sg/r/combo-box.jsonComboBox 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, ornull/truefor valid.isInvalid+errorMessage— use together for realtime validation feedback without waiting for a form submit.errorMessageaccepts aReactNodeor 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'sKey(ornullwhen 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-labelledbyandaria-describedby. - If no visible label is provided, pass
aria-labeloraria-labelledbyto theComboBox. - 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 whenonClearis provided.emptyState: The wrapper for the empty-state message (applied toComboBoxEmptyState).
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
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | ((item: T) => ReactNode) | - | Static children or render function for dynamic items |
label | ReactNode | - | The visible label for the field |
items | T[] | - | The list of items (controlled dynamic collection) |
defaultItems | T[] | - | The default list of items (uncontrolled dynamic collection) |
inputValue | string | - | The current text input value (controlled) |
defaultInputValue | string | - | The default text input value (uncontrolled) |
onInputChange | (value: string) => void | - | Called when the input value changes |
selectedKey | Key | null | - | The key of the selected item (controlled) |
defaultSelectedKey | Key | - | The default selected key (uncontrolled) |
onSelectionChange | (key: Key | null) => void | - | Called when the selection changes |
isDisabled | boolean | false | Whether the field is disabled |
isReadOnly | boolean | false | Whether the field is read-only |
isRequired | boolean | false | Whether the field is required |
isInvalid | boolean | false | Whether the field should display as invalid |
validate | (value: ComboBoxValidationValue) => ValidationError | true | null | undefined | - | Custom validation function |
errorMessage | ReactNode | ((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 |
classNames | SlotsToClasses<ComboBoxSlots & "clearButton" | "emptyState"> | - | Custom Tailwind classes for component slots |
description | ReactNode | null | - | Helper text shown below the field |
dependencies | any[] | - | Values that invalidate the item cache when using dynamic collections |
listLayoutOptions | ListLayoutOptions | - | 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 |
inputProps | Partial<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:
| Property | Type | Description |
|---|---|---|
isInvalid | boolean | Whether the field is currently invalid |
validationErrors | string[] | List of validation error strings |
validationDetails | ValidityState | The native HTML ValidityState object |
renderEmptyState render-prop argument
When renderEmptyState is a function, it receives a ListBoxRenderProps object:
| Property | Type | Description |
|---|---|---|
isEmpty | boolean | Whether the collection has no items |
isFocused | boolean | Whether the listbox is focused |
isDropTarget | boolean | Whether the listbox is a drag-and-drop target |
ComboBoxItem
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | ((renderProps: ListBoxItemRenderProps) => ReactNode) | - | The item content, or a render function |
id | Key | - | A unique identifier for the item; used as the value passed to onSelectionChange |
textValue | string | - | Plain-text value for filtering and accessibility; required when children is not a string |
description | ReactNode | - | Secondary descriptive text shown below the item label |
isDisabled | boolean | false | Whether the item is disabled |
classNames | SlotsToClasses<"container" | "label" | "description"> | - | Custom Tailwind classes for item slots |
Inherits all remaining props from React Aria's ListBoxItem.
ComboBoxEmptyState
| Prop | Type | Default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | - | Size variant; inherits from the parent ComboBox automatically |
className | string | - | 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.
Checkbox
A checkbox allows a user to select multiple items from a list of individual items, or to mark one individual item as selected.
DateField
A date field allows users to enter and edit date and time values using a keyboard. Each part of a date value is displayed in an individually editable segment.