SearchField
Free-text search with a clear button. For selecting from a constrained list, use ComboBox.
import { SearchField } from "@opengovsg/oui"export const Example = () => { return <SearchField aria-label="Search" />}Usage
Use SearchField for free-text queries (filtering a results list, searching across multiple sources). Use ComboBox when search results come from a known, constrained list.
import { SearchField } from "@opengovsg/oui"<SearchField aria-label="Search" />Alternatively, install the component as local source via the shadcn CLI:
npx shadcn@latest add https://oui.open.gov.sg/r/search-field.jsonpnpm dlx shadcn@latest add https://oui.open.gov.sg/r/search-field.jsonnpx shadcn@latest add https://oui.open.gov.sg/r/search-field.jsonbunx --bun shadcn@latest add https://oui.open.gov.sg/r/search-field.jsonThe SearchField component provides an accessible search input with a built-in search icon and clear button. The clear button automatically appears when the field has a value and hides when empty.
If the component does not have a visible label (by passing a label prop), an aria-label or aria-labelledby prop must be passed instead to identify it to assistive technology.
Examples
With Label and Description
Provide clear instructions to users with labels and descriptions.
import { SearchField } from "@opengovsg/oui"export const Example = () => { return ( <SearchField label="Search documents" description="Enter a keyword to search for documents." /> )}With Error Message
Combine isInvalid and errorMessage props to show validation errors.
import { SearchField } from "@opengovsg/oui"export const Example = () => { return ( <SearchField label="Search documents" isInvalid errorMessage="Please enter a valid search query." /> )}Disabled
Use the isDisabled prop to disable the search field.
import { SearchField } from "@opengovsg/oui"export const Example = () => { return <SearchField isDisabled label="Search documents" />}Sizes
Use the size prop to change the size of the field. Defaults to "md".
import { SearchField } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex w-full flex-col gap-4"> <SearchField label="Search (xs)" size="xs" /> <SearchField label="Search (sm)" size="sm" /> <SearchField label="Search (md)" size="md" /> </div> )}With Placeholder
Use the inputProps prop to pass additional props to the underlying input element, such as a placeholder.
import { SearchField } from "@opengovsg/oui"export const Example = () => { return ( <SearchField label="Search" inputProps={{ placeholder: "Search by name or email" }} /> )}Custom Search Icon
Use the searchIcon prop to replace the default search icon with a custom one.
import { SearchField } from "@opengovsg/oui"import { FilterIcon } from "lucide-react"export const Example = () => { return ( <SearchField label="Filter" searchIcon={ <FilterIcon aria-hidden className="text-base-content-medium ml-4 size-5" /> } /> )}Hidden Search Icon
Set the searchIcon prop to null to hide the icon entirely.
import { SearchField } from "@opengovsg/oui"export const Example = () => { return <SearchField label="Search" searchIcon={null} />}Custom Clear Icon
Use the clearIcon prop to replace the default clear (X) icon. This is useful for showing a loading spinner during async search operations.
import { SearchField } from "@opengovsg/oui"import { LoaderCircleIcon } from "lucide-react"export const Example = () => { return ( <SearchField label="Async search" defaultValue="Searching..." isReadOnly={true} clearIcon={ <LoaderCircleIcon className="animate-spin" aria-label="Loading" /> } /> )}With Action Element
Use the actionElement prop to render an element beside the input group, such as a search button or filter button.
import { Button, SearchField } from "@opengovsg/oui"import { SearchIcon } from "lucide-react"export const Example = () => { return ( <SearchField label="Search" classNames={{ fieldWrapper: "gap-1", }} actionElement={ <Button size="md" aria-label="Search" onPress={() => alert("Search button clicked")} > <SearchIcon /> Search </Button> } /> )}Controlled
Use the value and onChange props to control the input value. The onSubmit callback is invoked when the user presses Enter.
import { SearchField } from "@opengovsg/oui"import { useState } from "react"export const Example = () => { const [value, setValue] = useState("") const [submittedValue, setSubmittedValue] = useState<string | null>(null) return ( <div className="flex w-full flex-col gap-y-2"> <SearchField label="Controlled search" value={value} onChange={setValue} onSubmit={() => setSubmittedValue(value)} /> <p className="text-base-content-medium text-sm"> Input value: <strong>{value || "–"}</strong> </p> <p className="text-base-content-medium text-sm"> Submitted value: <strong>{submittedValue ?? "–"}</strong> </p> </div> )}Validation
See the Forms guide for end-to-end patterns including React Hook Form + Zod.
SearchField supports React Aria's form validation system:
- Built-in constraints: Use
isRequired,minLength,maxLength, orpatternfor native HTML validation. Errors display after the user commits a value or submits the form. - Custom validation: Use the
validateprop to provide a function that returns an error message string, ornull/trueif valid. - Realtime validation: Use the
isInvalidprop witherrorMessagefor immediate feedback as the user types. - Validation behavior: The
validationBehaviorprop controls enforcement —"native"(default) prevents form submission when invalid, while"aria"only marks the field via ARIA attributes and allows submission.
Events
SearchField supports the following event handlers, inherited from React Aria's SearchField:
onChange— Called when the value changes, receiving the new string value.onSubmit— Called when the user presses Enter, receiving the current value.onClear— Called when the user clears the field (by pressing the clear button or Escape).onFocus/onBlur— Called when the input gains or loses focus.onFocusChange— Called when the focus state changes, receiving a boolean.onKeyDown/onKeyUp— Called on keyboard events.
Accessibility
- Built on a native
<input type="search">element for full browser and assistive technology support. - Label, description, and error message are linked to the input via
aria-labelledbyandaria-describedby. - If no visible label is provided, an
aria-labeloraria-labelledbyprop must be set. - The clear button is keyboard-accessible; pressing Escape while the field has a value clears it.
- Required and invalid states are exposed to assistive technology.
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.
base: The root container wrapping the entire field (label, input group, description, error).label: The label element rendered above the input.fieldWrapper: The flex wrapper containing the input group and the optional action element.group: The bordered input group containing the search icon, input, and clear button.searchIcon: The leading search icon.input: The native<input type="search">element.clearButton: The trailing clear button.description: The description text rendered below the input.error: The error message text rendered below the input.
Props
SearchField
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | - | The visible label for the field |
description | ReactNode | - | Description text rendered below the input |
errorMessage | ReactNode | ((v: ValidationResult) => ReactNode) | - | The error to display when invalid; accepts a render prop for custom error messages |
value | string | - | The current value (controlled) |
defaultValue | string | - | The default value (uncontrolled) |
onChange | (value: string) => void | - | Called when the value changes |
onSubmit | (value: string) => void | - | Called when the user presses Enter |
onClear | () => void | - | Called when the user clears the field |
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: string) => ValidationError | true | null | undefined | - | Custom validation function |
validationBehavior | "native" | "aria" | "native" | Native HTML validation vs ARIA-only |
size | "xs" | "sm" | "md" | "md" | The size of the field |
variant | "outline" | "unstyled" | "outline" | The visual variant of the input |
actionElement | ReactNode | - | An element rendered beside the input group (e.g., a search button) |
searchIcon | ReactNode | null | <SearchIcon /> | Custom leading icon, or null to hide |
clearIcon | ReactNode | <XIcon /> | Custom clear-button icon (e.g., a spinner during async search) |
inputProps | Partial<InputProps> | - | Additional props forwarded to the underlying <input> (e.g., placeholder) |
classNames | SlotsToClasses<SearchFieldSlots> | - | Per-slot Tailwind classes for custom styling |
Inherits all remaining props from React Aria's SearchField. Notable ones: autoFocus, excludeFromTabOrder, name, autoComplete.