Search
A search field with a leading glyph, a clear button, submit-on-Enter and optional suggestions — the one Input a site puts in its header, wrapped in the search landmark.
Basic usage
Install the package:
pnpm add @design-schema/react @design-schema/tokens Import the stylesheet once for the app, then the component:
import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { Search } from '@design-schema/react'; Examples
import { Search } from '@design-schema/react';
<Search label="Search products" />Props
12 rows| Prop | Type | Default | Description |
|---|---|---|---|
| label required | string | — | The accessible name ("Search products", "Search this site"). Visually hidden by default — the glyph and placeholder are the visible cue. Accessibility: label/for on the input, visually hidden unless `showLabel`. |
| showLabel | boolean | false | Show the label above the field, as in a search page rather than a header. |
| name | string | q | Field name; the query key when the form submits to a URL. |
| value | string | — | Controlled query. When set, it is the query everything reads: submit, a chosen suggestion, the clear button and the Form value all use this prop, never stale typed text; clearing reports onChange("") and the field empties when the caller passes the new value. |
| defaultValue | string | — | Initial query. |
| placeholder | string | — | Example query, not a label ("Try "invoices from March""). |
| action | string | — | URL to submit to with GET (web and Lit); when omitted, `onSubmit` handles it and nothing navigates. The URL carries the same trimmed query `onSubmit` receives (the field value is trimmed before the native submit): the visible input carries no `name`; a hidden input named `name` is set to the trimmed query (the controlled value or chosen label, never stale DOM text) in the submit handler. Ignored, with a development warning, when Search sits inside a Form component: the enclosing Form owns submission. |
| suggestions | array { value: string; label: string; description?: string }[] | — | Suggestions for the current query, shown in a Listbox under the field; choosing one fills the query with the suggestion's `label` — what the user just read — and submits. Provide them from `onChange` (debounced by the caller). Setting the prop at all is what turns the field into a combobox, including an explicitly empty array after a fetch that found nothing, which shows `copy.noSuggestions`; leaving it undefined keeps a plain search field. With suggestions the field becomes a Combobox: same keys, `aria-activedescendant` on web (Lit uses a live span instead; see platforms.lit). `aria-controls` and `aria-activedescendant` are set only while the list is open, so neither ever points at an element that is not in the document. The composed Listbox gets `label` = Search's `label`, `embedded`, `value: ""` (no persistent selection after a choice), the suggestions as options and, while empty, `emptyMessage` = `copy.noSuggestions` (or `copy.loading` while `loading`); Listbox's own `loading` prop is never used. Wiring props the combobox pattern needs are passed too and are not part of that list: an `id` for aria-controls and option ids, `selectionFollowsFocus: false` so a highlight never selects, `onChange`/`onActiveChange`, and a key that remounts it to clear the highlight; `labelledBy` is not used. The list opens on typing (while `suggestions` is set) and on ArrowDown — never on focus alone, and a `suggestions` array arriving while the field is focused but untouched does not open it. It closes on Escape, on Tab (explicitly, even though Tab moves focus to the clear or submit Button, which is inside Search), on blur to an element outside Search (a blur with no new focus target, such as a window switch, does not close it), on a pointer press outside Search — the same scope as the blur rule, so a press on a shown label or on the submit Button does not close it — on a chosen suggestion, on clear, and on every submit attempt, including one an empty query refuses to send. ArrowDown on the last suggestion stays there (no wrap), as ArrowUp never wraps. The popup is at least as wide as the field (a `min-inline-size` from the field's measured width, as Combobox) and a long suggestion label may widen it. |
| loading | boolean | false | Suggestions are being fetched; announced through `copy.loading` whenever `suggestions` is set and this is true, list open or not — a fetch the user triggered is worth hearing about. The count and `copy.noSuggestions` announce only while the list is open. |
| landmark | boolean | true | Give the field the `search` landmark. Turn off when the Search sits inside another search landmark (a filter within a results page). It is `role="search"` on Search's own form element, not a composed Landmark wrapping it: the landmark is one attribute on an element this component already renders, and a wrapper would add a second element and take the root's testability hook. With a single search landmark on the page it needs no name of its own beyond the field's label. |
| size | md | lg | md | lg for a search page's hero field. |
| disabled | boolean | false | Not editable, still readable and focusable: the input is read-only with aria-disabled (on native `editable={false}` with accessibilityState disabled, which as Input is not focusable on iOS), both Buttons are disabled (the clear Button still renders when there is text), every key in the keyboard table is inert, suggestions never open and an open list closes, no event fires, the component dims to `disabledOpacity`, and a disabled Search is not registered with (or submitted by) a Form — on Lit ds-form skips it as disabled and it sends setFormValue(null). A Form-level disabled (the Form's own `disabled`, FormContext) unregisters it the same way, rather than registering and reporting itself disabled. |
Events
3 rows| Event | Type | Description |
|---|---|---|
| onChange | onChange / change / onChangeText | Fired on every keystroke with the query; the caller fetches suggestions here. Also fired whenever Search itself changes the text, so a controlled value can follow: with "" before `onClear` (clear button or Escape), and with the suggestion's `label` before `onSubmit` when one is chosen. |
| onSubmit | onSubmit / submit / onSubmitEditing | Fired on Enter, the submit button, or choosing a suggestion, with the query. |
| onClear | onClear / clear | Fired when the field is emptied — by the clear button, or by the Escape that clears it when no suggestions are open. Either route reports. |
Accessibility
Decisions this component leaves to you
Supply the label. It is associated with the control for you, but the words are yours — a placeholder is not a label.
Supply an accessible name. This component has no text of its own to fall back on, so without one a screen reader announces its role and nothing else.
Choose the landmark role, and give it a name when the page has more than one landmark of that role.
Required props that carry accessibility meaning: label. Each is described in the props table above.
Implicit ARIA role: searchbox.
Anatomy
What generation guarantees
Label association — The label is programmatically associated with the control, not merely placed beside it.
Accessible name — Every instance carries a programmatic name. Where the component has no text of its own to use, the name is a prop the consumer supplies.
Landmark — It renders a landmark, so assistive technology can jump straight to it.
Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.
Arrow navigation — Arrow keys move between items; Home and End jump to the ends.
Visible focus — Keyboard focus is always drawn, from the theme’s focus tokens, and is never removed.
Contrast — Text and meaningful non-text contrast meets WCAG AA; the pairs are listed below.
Target size — Interactive targets are at least 24×24 CSS pixels (WCAG 2.2 Target Size, minimum).
Live region — Changes are announced through a live region, without moving focus.
Escape dismisses — Escape closes it from anywhere inside.
This component does not animate, so it has no reduced-motion behaviour to describe.