SearchField
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/search-field.jsonAn Input with three small things the products kept not doing, so they are done once here rather than four times badly.
A magnifier, so the field is recognisable before it is read.
A way to clear it that is not “select all and delete” — and one a keyboard
can reach. This is the half that goes wrong: the cross is usually a decorative
<span> with an onClick, which Tab skips and a screen reader does not
announce, so the only way to empty the field is the one the mouse knows. Here
it is a real <button> with a name, and clearing puts the cursor back in the
field, because clearing and then having to click the field again is half a
feature.
The shortcut that focuses it, shown in the field rather than learned. It is declared as a command, so it is listed in the sheet of shortcuts and the palette like every other key, and the hint shows the keys it answers to now - a rebinding moves the key and the hint together.
type="search" is deliberate
Section titled “type="search" is deliberate”It is what tells a browser to offer previous queries, and what makes Escape
clear the field on the platforms where that is the convention. The browser’s
own clear button is hidden, because it is drawn in the operating system’s
chrome and cannot be made to match — the same reason the line does not use a
native <select>.
The two props that are switches
Section titled “The two props that are switches”clearLabel is the clear button. Leave it out and no button is drawn, which
is the right shape for a field that filters as you type and is emptied by
other means. There is no separate showClear flag, because a flag can
disagree with the label, and a button announced as nothing is worse than no
button at all.
shortcut is the same idea: give it one and it is shown, bound and listed;
leave it out and none of that happens.
The hint and the clear button share the right edge, so the hint gives way as soon as there is something to clear.
The shortcut does not fire while someone is typing
Section titled “The shortcut does not fire while someone is typing”It is a command underneath, with the default
intact: Mod+K typed into another field belongs to that field. It focuses
and selects, so the shortcut replaces a stale query rather than appending to
it.
import { SearchField } from '@/components/ui/search-field'
<SearchField value={query} onValueChange={setQuery} aria-label={t('search')} placeholder={t('searchPlaceholder')} clearLabel={t('clear')} shortcut={{ id: 'search', label: t('search'), keys: 'Mod+K' }}/>SearchField or Combobox
Section titled “SearchField or Combobox”SearchField when what is typed filters something the reader is already looking at — a table, a list of files, a page of results. There is nothing to choose from a popup; the query is the whole interaction.
Combobox when the typing ends in a choice: the reader is picking one of a set, and the query is only how they find it.
CommandPalette when it is the same box for the whole application, opened by a shortcut from anywhere.
Every other <input> attribute goes to the input - placeholder, aria-label,
disabled, name and the rest. type is not among them: it is always
search.
Props
Read from the types in ui/search-field.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.
| Prop | Type | Default | |
|---|---|---|---|
valuerequired | string | The query. Controlled, because a search box that owns its own text cannot be cleared by the thing that owns the results. | |
onValueChangerequired | (value: string) => void | Told the new query on every keystroke. | |
clearLabel | string | What the clear button is called, for a screen reader. No default: a word the component invents is a word the product cannot translate. | |
shortcut | SearchShortcut | The command that focuses the field: { id: 'search', label: 'Search', keys: 'Mod+K' }. Declared like any other command, so it is listed in the sheet of shortcuts and the palette, and a rebinding moves the key and the hint together. The hint at the right of the field shows the keys it answers to now, not the ones it was written with. | |
ref | Ref<HTMLInputElement> | A ref to the <input> itself, not the wrapper around it. |
And every prop of React's InputHTMLAttributes<HTMLInputElement> except type, value, onChange, passed on.
Accessibility
The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 15 rules applied and passed, and none failed. The accessibility report has every item.
History
Each release that changed ui/search-field.tsx - the file a product copies - newest first.