Skip to content

SearchField

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/search-field.json
See it liveEmpty with its shortcut showing, and with a query and the clear button in its place.→

An 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.

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>.

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 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.

PropTypeDefault
valuerequiredstringThe query. Controlled, because a search box that owns its own text cannot be cleared by the thing that owns the results.
onValueChangerequired(value: string) => voidTold the new query on every keystroke.
clearLabelstringWhat the clear button is called, for a screen reader. No default: a word the component invents is a word the product cannot translate.
shortcutSearchShortcutThe 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.
refRef<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.

  1. v0.41.0
    • docs read every props table from the component's types ee3b159
  2. v0.35.0breaking
    • feat one list of commands for keys, palette and sheet dd9a125
  3. v0.28.0breaking
    • feat density as a property of a region 849d783
  4. v0.10.0
    • feat CommandPalette, SearchField and the shortcut behind them 8155561