TagField
A type-ahead input for selecting multiple items from a list, rendering each as a removable tag. For single-select with filtering, use ComboBox.
import { TagField } 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" }, ] return <TagField label="Engineering" defaultItems={options} />}Usage
Use TagField when users select multiple items from a known list. Use ComboBox for single-select with filtering. Use TagGroup when the tags are not interactive selections (display-only).
import { TagField } from "@opengovsg/oui"<TagField
label="Engineering disciplines"
defaultItems={[
{ id: 1, textValue: "Aerospace" },
{ id: 2, textValue: "Mechanical" },
{ id: 3, textValue: "Civil" },
]}
/>Alternatively, install the component as local source via the shadcn CLI:
npx shadcn@latest add https://oui.open.gov.sg/r/tag-field.jsonpnpm dlx shadcn@latest add https://oui.open.gov.sg/r/tag-field.jsonnpx shadcn@latest add https://oui.open.gov.sg/r/tag-field.jsonbunx --bun shadcn@latest add https://oui.open.gov.sg/r/tag-field.jsonTagField combines a text input with a virtualised listbox to let users pick multiple items from a list. Each selected item is rendered as a removable tag. The component is built on Downshift for combobox behaviour and uses React Aria utilities for label/description/error wiring.
If the tag field does not have a visible label, an aria-label or aria-labelledby prop must be passed instead to identify it to assistive technology.
Anatomy
TagField consists of:
- TagField — the root component; manages state, the text input, and the dropdown
- TagFieldItem — one option row in the dropdown list (used internally by default; you can replace it via the
childrenrender prop)
- TagFieldItem — one option row in the dropdown list (used internally by default; you can replace it via the
Examples
Sizes
Use the size prop to change the size of the tag field. Defaults to "md".
import { TagField } from "@opengovsg/oui"export const Example = () => { const options = [ { id: "red panda", textValue: "Panda" }, { id: "cat", textValue: "Cat" }, { id: "dog", textValue: "Dog" }, { id: "aardvark", textValue: "Aardvark" }, { id: "kangaroo", textValue: "Kangaroo" }, { id: "snake", textValue: "Snake" }, ] return ( <div className="flex flex-col space-y-4"> <TagField label="Favorite Animal (xs)" defaultItems={options} size="xs" defaultSelectedKeys={new Set(["cat"])} /> <TagField label="Favorite Animal (sm)" defaultItems={options} size="sm" defaultSelectedKeys={new Set(["dog"])} /> <TagField label="Favorite Animal (md)" defaultItems={options} size="md" defaultSelectedKeys={new Set(["aardvark"])} /> </div> )}Controlled
Use selectedKeys and onSelectionChange to control the selection programmatically. selectedKeys is a Set<Key>; onSelectionChange receives the updated set after every change.
import { TagField } from "@opengovsg/oui"import { useState } from "react"import type { Key } from "react-aria-components"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 [selectedIds, setSelectedIds] = useState<Set<Key>>(new Set()) return ( <div className="flex flex-col"> <TagField label="Engineering" defaultItems={options} selectedKeys={selectedIds} onSelectionChange={setSelectedIds} /> <p>Selected ids: {[...selectedIds].join(", ")}</p> </div> )}Checklist
Selected options always stay in the dropdown list instead of being filtered out, so clicking an already-selected option deselects it again. Set shouldCloseOnSelect={false} to also keep the dropdown open after each pick, letting users select several options in one visit without reopening the menu. Pair it with showCheckbox to render a checkbox on every option indicating its selected state — the checkbox stays aligned to an option's first line even when its label wraps across multiple lines.
import { TagField } from "@opengovsg/oui"export const Example = () => { const options = [ { id: "red panda", textValue: "Panda" }, { id: "cat", textValue: "Cat" }, { id: "dog", textValue: "Dog" }, { id: "aardvark", textValue: "Aardvark" }, { id: "kangaroo", textValue: "Kangaroo" }, { id: "snake", textValue: "Snake" }, ] return ( <TagField label="Favorite animals" defaultItems={options} shouldCloseOnSelect={false} showCheckbox /> )}With Description
Use the description prop for helper text below the input.
import { TagField } 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" }, ] return ( <TagField label="Engineering" defaultItems={options} description="Choose one of the above options." /> )}Custom Item Rendering
Pass a render function to children to fully customise each option row. The function receives a TagFieldRenderProps<T> object — spread itemProps onto your root element and use key as the React key. Set virtualRowHeight to the actual row height when custom items are taller than the default.
import { TagField } from "@opengovsg/oui"import { cn } from "@opengovsg/oui-theme"import { useState } from "react"import type { Key } from "react-aria-components"export const Example = () => { const options = [ { id: 1, textValue: "Aerospace", description: "Aerospace engineering" }, { id: 2, textValue: "Mechanical", description: "Mechanical engineering" }, { id: 3, textValue: "Civil", description: "Civil engineering" }, { id: 4, textValue: "Biomedical", description: "Biomedical engineering" }, { id: 5, textValue: "Nuclear", description: "Nuclear engineering" }, { id: 6, textValue: "Industrial", description: "Industrial engineering" }, { id: 7, textValue: "Chemical", description: "Chemical engineering" }, { id: 8, textValue: "Electrical", description: "Electrical engineering" }, ] const [selectedIds, setSelectedIds] = useState<Set<Key>>(new Set()) return ( <div className="flex flex-col"> <TagField label="Engineering" defaultItems={options} selectedKeys={selectedIds} onSelectionChange={setSelectedIds} virtualRowHeight={64} // Important if you have custom children > {({ itemProps, key, item, isHighlighted, classNames }) => ( <div key={key} {...itemProps} className={cn( classNames?.container, "flex flex-col p-2", isHighlighted && "bg-amber-100", )} > <span className={cn(classNames?.label)}>{item.textValue}</span> <span className={cn(classNames?.description, "text-gray-600")}> {item.description} </span> </div> )} </TagField> <p>Selected types: {[...selectedIds].join(", ")}</p> </div> )}Disable Virtualisation
By default TagField virtualises its list items for performance. Set isVirtualized={false} to render items in normal DOM flow — useful for small lists or when absolute positioning conflicts with animations or variable-height items.
import { TagField } 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" }, ] return ( <TagField label="Engineering" defaultItems={options} isVirtualized={false} /> )}Disabled
Use isDisabled to disable the entire field.
import { TagField } from "@opengovsg/oui"export const Example = () => { const options = [ { id: "red panda", textValue: "Panda" }, { id: "cat", textValue: "Cat" }, { id: "dog", textValue: "Dog" }, ] return <TagField label="Favorite Animal" defaultItems={options} isDisabled />}Disabled Options
Use disabledKeys to disable specific options by their key.
import { TagField } from "@opengovsg/oui"export const Example = () => { const options = [ { id: "red panda", textValue: "Panda" }, { id: "cat", textValue: "Cat" }, { id: "dog", textValue: "Dog" }, { id: "aardvark", textValue: "Aardvark" }, { id: "kangaroo", textValue: "Kangaroo" }, { id: "snake", textValue: "Snake" }, ] return ( <TagField label="Favorite Animal" defaultItems={options} disabledKeys={["cat", "kangaroo"]} /> )}Validation
Use isInvalid and errorMessage together to show an error state.
import { TagField } from "@opengovsg/oui"export const Example = () => { const options = [ { id: "red panda", textValue: "Panda" }, { id: "cat", textValue: "Cat" }, { id: "dog", textValue: "Dog" }, { id: "aardvark", textValue: "Aardvark" }, { id: "kangaroo", textValue: "Kangaroo" }, { id: "snake", textValue: "Snake" }, ] return ( <TagField label="Favorite Animal" isInvalid errorMessage="Please select a valid animal" defaultItems={options} disabledKeys={["cat", "kangaroo"]} /> )}Content
Unlike most Collection Components, TagField only accepts the dynamic collection method — you must pass an iterable of items via defaultItems (uncontrolled) or items (controlled). Static children are not supported.
// Uncontrolled — items list never changes
<TagField
label="Engineering"
defaultItems={[
{ id: 1, textValue: "Aerospace" },
{ id: 2, textValue: "Mechanical" },
]}
/>
// Controlled — swap in a filtered list externally
<TagField
label="Engineering"
items={filteredItems}
inputValue={inputValue}
onInputChange={setInputValue}
/>Each item object must satisfy one of:
- Has an
idfield — used automatically as the item's key (value passed toonSelectionChange). - Has a
textValuefield — used as the display text and filter target. - Neither — provide
itemToKeyanditemToTextfunctions to extract the key and display text from your item shape.
import { TagField } from "@opengovsg/oui"import { useState } from "react"import type { Key } from "react-aria-components"export const Example = () => { const options = [ { email: "test1@example.com", name: "Test Example 1" }, { email: "test2@example.com", name: "Test Example 2" }, { email: "test3@example.com", name: "Test Example 3" }, { email: "test4@example.com", name: "Test Example 4" }, { email: "test5@example.com", name: "Test Example 5" }, ] const [selectedIds, setSelectedIds] = useState<Set<Key>>(new Set()) return ( <div className="flex flex-col"> <TagField label="Users to notify" defaultItems={options} selectedKeys={selectedIds} onSelectionChange={setSelectedIds} itemToKey={(item) => item.email} itemToText={(item) => item.name} /> <p>Selected emails: {[...selectedIds].join(", ")}</p> </div> )}Validation
See the Forms guide for end-to-end patterns including React Hook Form + Zod.
isRequired— marks the field as required; combined with native form submission or"aria"validation behaviour.validate— a custom function(value: TagFieldValidationValue) => ValidationError | true | null | undefined.TagFieldValidationValuehas{ selectedKeys: Set<Key> | null, inputValue: string }. Return a string to show as the error, ornull/truefor valid.isInvalid+errorMessage— use together for realtime validation feedback without waiting for form submission.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
onSelectionChange— Called when the selection changes, receiving the full updatedSet<Key>.onInputChange— Called when the text input value changes, receiving the new string.onOpenChange— Called when the dropdown opens or closes, receivingisOpen: boolean.onFocus— Called when the input receives focus.onBlur— Called when the input loses focus.onFocusChange— Called when the focus state changes, receiving a boolean.
Accessibility
- TagField uses a native
<input>with ARIA combobox semantics (rolecombobox,aria-haspopup,aria-expanded) managed by Downshift. - The label, description, and error message are automatically wired via
aria-labelledbyandaria-describedbythrough React Aria'suseTextField. - If no visible label is provided, pass
aria-labeloraria-labelledbyto theTagField. - Keyboard interaction: type to filter options; Up/Down to navigate the list; Enter to select (or deselect, if already selected); Backspace on empty input removes the last tag; Escape closes the dropdown.
- Each selected tag is a focusable element; Backspace or Delete on a focused tag removes it.
- With
shouldCloseOnSelect={false}, the highlighted option stays highlighted after selecting or deselecting it, instead of resetting to the first option in the list.
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.
TagField slots:
root: The outermost flex-column container wrapping the label, field group, description, and error.label: The label element rendered above the field group.group: The bordered input group that wraps the tags, text input, and trigger button.tag: Each selected-item chip rendered inside the group.tagText: The text span inside each tag chip.tagIcon: The remove icon button inside each tag chip.field: The<input>element used for filtering.trigger: The chevron button that opens and closes the dropdown.description: The helper text rendered below the group.error: The error message rendered below the group when the field is invalid.popover: The floating popover container.list: The scrollable list inside the popover.
TagFieldItem slots (via TagFieldItem's classNames or the itemClassNames prop on TagField):
container: The list item row.label: The primary text label inside the item row.description: The secondary description text inside the item row (unused in the default renderer; available for custom item layouts).checkboxBox: The checkbox square rendered whenshowCheckboxistrue. Vertically centered against the label's first line so it stays aligned when a label wraps across multiple lines.checkboxIcon: The checkmark icon insidecheckboxBox, shown when the option is selected.
Custom Styles
import { TagField } 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" }, ] return ( <TagField label="Engineering" defaultItems={options} classNames={{ label: "text-purple-600 font-semibold", group: "border-2 border-purple-400 focus-within:border-purple-600", tag: "bg-purple-100 text-purple-700", }} /> )}Props
TagField
| Prop | Type | Default | Description |
|---|---|---|---|
children | (values: TagFieldRenderProps<T>) => ReactNode | - | Custom render function for each dropdown option; see render-prop table below |
label | ReactNode | - | The visible label for the field |
items | T[] | - | Controlled list of items for the dropdown |
defaultItems | T[] | - | Uncontrolled initial list of items for the dropdown |
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 |
selectedKeys | Set<Key> | - | The set of selected item keys (controlled) |
defaultSelectedKeys | Set<Key> | - | The default set of selected keys (uncontrolled) |
onSelectionChange | (keys: Set<Key>) => 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: TagFieldValidationValue) => ValidationError | true | null | undefined | - | Custom validation function |
errorMessage | ReactNode | ((validation: ValidationResult) => string) | - | Error message displayed when the field is 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<TagFieldSlots> | - | Custom Tailwind classes for component slots |
itemClassNames | SlotsToClasses<TagFieldItemSlots> | - | Custom Tailwind classes applied to every default TagFieldItem row |
itemToText | (item: T) => string | item => item.textValue | Extracts the display text from an item; required when items lack a textValue field |
itemToKey | (item: T) => Key | item => item.id | Extracts the unique key from an item; required when items lack an id field |
defaultFilter | (textValue: string, inputValue: string) => boolean | case-insensitive contains | Filter function for uncontrolled collections; receives the item text and current input |
virtualRowHeight | number | size-dependent | Override the estimated height of each virtualised row in pixels |
isVirtualized | boolean | true | Whether to virtualise the list; set false for small lists or variable-height items |
shouldCloseOnBlur | boolean | true | Whether the dropdown closes when the input loses focus |
shouldCloseOnSelect | boolean | true | Whether the dropdown closes when an option is selected; set false to select multiple options in one visit, with the picked option staying highlighted |
showCheckbox | boolean | false | Whether every option shows a checkbox indicating its selected state; a future major version may default this to true |
description | ReactNode | null | - | Helper text shown below the field |
placeholder | string | - | Placeholder text for the text input |
disabledKeys | Iterable<Key> | - | Keys of items that cannot be selected |
onOpenChange | (isOpen: boolean) => void | - | Called when the dropdown opens or closes |
onFocus | (e: FocusEvent<HTMLInputElement>) => void | - | Called when the input receives focus |
onBlur | (e: FocusEvent<HTMLInputElement>) => void | - | Called when the input loses focus |
onFocusChange | (isFocused: boolean) => void | - | Called when the focus state changes |
Inherits autoFocus and other focusable props from React Aria's FocusableProps.
children render-prop argument (TagFieldRenderProps<T>)
When children is a function, it is called once per visible dropdown row with:
| Property | Type | Description |
|---|---|---|
item | T | The item data object for this row |
key | string | number | The React key for this row; must be passed as the key prop on the root element |
isHighlighted | boolean | Whether this row is currently keyboard-highlighted |
isSelected | boolean | Whether this row is currently selected |
itemProps | TagFieldBaseItemProps<T> | ARIA and event props from Downshift; spread onto the root element of your custom row |
classNames | SlotsToClasses<TagFieldItemSlots> | The resolved slot classes from itemClassNames; apply to container, label, description, checkboxBox, checkboxIcon spans as needed |
TagFieldItem
TagFieldItem is the default option renderer used internally. You can use it directly inside a custom children function when you only need partial customisation.
| Prop | Type | Default | Description |
|---|---|---|---|
item | T | - | The item data object |
isHighlighted | boolean | false | Whether the item is keyboard-highlighted |
isSelected | boolean | false | Whether the item is currently selected; drives the checkbox's checked state when shown |
classNames | SlotsToClasses<TagFieldItemSlots> | - | Custom Tailwind classes for item slots (container, label, description, checkboxBox, checkboxIcon) |
style | React.CSSProperties | - | Inline styles (used internally by the virtualiser for positioning) |
Spread all remaining TagFieldBaseItemProps<T> (ARIA + event props from Downshift) onto the <li> element.
Internal: TagFieldRoot render-prop state
TagFieldRoot is an internal (non-exported) primitive that TagField delegates to. Its children render-prop function receives additional state properties for advanced headless composition — these are not accessible through TagField's public API but are listed here for completeness:
| Property | Type | Description |
|---|---|---|
highlightedIndex | number | undefined | The index of the currently keyboard-highlighted option in the dropdown list |
selectedItems | T[] | Array of the currently selected item objects (mirrors the selectedKeys set resolved to objects) |
removeSelectedItem | (item: T) => void | Imperatively removes an item from the selection |
Tabs
Organizes content into multiple panels and lets users switch between them. For page-level navigation, use a nav element or breadcrumbs instead.
TagGroupWIP
A tag group is a focusable list of labels, categories, keywords, filters, or other items, with support for keyboard navigation, selection, and removal.