The field that finds a line or a stop: a labelled text input with the search icon, a clear button while it holds text, and a list of suggestions under it that narrows as the person types.
When to use
Use SearchField to find a line or a stop among many, with suggestions that narrow as the person types.
Kongens Nytorv
Metro M1, M2 and M3.
Props
| Name | Type | Default | Description |
|---|---|---|---|
label* | ReactNode | — | Required. The field's name, in sentence case, such as "Search lines and stops": shown above the box, and the input's accessible name. |
hiddenLabel | boolean | false | Whether the label is heard but not seen. Use it where the icon and the placeholder make the field's purpose plain, as in the top bar. |
placeholder | string | — | Words shown in the empty box, such as "Line or stop, such as 5C". They vanish as the person types, so they never replace the label. |
items* | readonly SearchFieldItem[] | — | Required. The suggestions: each with a value that identifies it, the label shown and filled into the box when it is chosen, and an optional description and startIcon. |
filter | (item: SearchFieldItem, query: string) => boolean | null | — | Whether an item matches the typed words. Left out, an item matches when its label contains them, ignoring case and accents. null shows items as given, for suggestions the caller has already matched, such as from a server. |
value | string | — | The words in the box, when the caller controls them. |
defaultValue | string | — | The words in the box at first, when SearchField keeps them itself. Left out, the box starts empty. |
onChange | (event: Event, value: string, details: Autocomplete.Root.ChangeEventDetails) => void | — | Called when the words change: by typing (details.reason 'input-change'), by a chosen item filling them ('item-press'), by the clear button ('clear-press'), or by Esc with the list closed ('escape-key'). event is details.event. |
open | boolean | — | Whether the list of suggestions is shown, when the caller controls it. |
defaultOpen | boolean | false | Whether the list is shown at first, when open is not set. |
onOpen | (event: Event, details: Autocomplete.Root.OpenChangeEventDetails) => void | — | Called when the list asks to open: as the person types (details.reason 'input-change') or presses an arrow key ('list-navigation'). |
onClose | (event: Event, reason: Autocomplete.Root.ChangeEventReason, details: Autocomplete.Root.OpenChangeEventDetails) => void | — | Called when the list asks to close: reason is 'escape-key', 'outside-press', 'focus-out', 'item-press', 'input-clear', 'close-press', or 'none' for Enter with no row highlighted; details.cancel() keeps it open. |
onItemSelect | (event: React.MouseEvent<HTMLDivElement>, item: SearchFieldItem) => void | — | Called when the person chooses a suggestion, with a press or with Enter on the highlighted row, with the item itself. |
ref | Ref<HTMLDivElement> | — | Reaches the root, the Field.Root <div>. |
clearLabel | string | The locale's | Overrides "Clear", the clear button's name. |
noMatchesLabel | string | The locale's | Overrides "No matches", the message the list shows and announces when no suggestion matches. |
slotProps | { label?: FieldLabelProps | undefined; inputGroup?: AutocompleteInputGroupProps | undefined; input?: ComboboxInputProps | undefined; clear?: Omit<ComboboxClearProps, "disabled"> | undefined; portal?: ComboboxPortalProps | undefined; positioner?: ComboboxPositionerProps | undefined; popup?: ComboboxPopupProps | undefined; empty?: ComboboxEmptyProps | undefined; list?: ComboboxListProps | undefined; item?: AutocompleteItemProps | undefined; } | — | Props for each part: label, inputGroup, input, clear, portal, positioner, popup, empty, list, item. |
label (required)ReactNode, default —- Required. The field's name, in sentence case, such as "Search lines and stops": shown above the box, and the input's accessible name.
hiddenLabelboolean, defaultfalse- Whether the label is heard but not seen. Use it where the icon and the placeholder make the field's purpose plain, as in the top bar.
placeholderstring, default —- Words shown in the empty box, such as "Line or stop, such as 5C". They vanish as the person types, so they never replace the label.
items (required)readonly SearchFieldItem[], default —- Required. The suggestions: each with a
valuethat identifies it, thelabelshown and filled into the box when it is chosen, and an optionaldescriptionandstartIcon. filter(item: SearchFieldItem, query: string) => boolean | null, default —- Whether an item matches the typed words. Left out, an item matches when its label contains them, ignoring case and accents.
nullshowsitemsas given, for suggestions the caller has already matched, such as from a server. valuestring, default —- The words in the box, when the caller controls them.
defaultValuestring, default —- The words in the box at first, when SearchField keeps them itself. Left out, the box starts empty.
onChange(event: Event, value: string, details: Autocomplete.Root.ChangeEventDetails) => void, default —- Called when the words change: by typing (
details.reason'input-change'), by a chosen item filling them ('item-press'), by the clear button ('clear-press'), or by Esc with the list closed ('escape-key').eventisdetails.event. openboolean, default —- Whether the list of suggestions is shown, when the caller controls it.
defaultOpenboolean, defaultfalse- Whether the list is shown at first, when
openis not set. onOpen(event: Event, details: Autocomplete.Root.OpenChangeEventDetails) => void, default —- Called when the list asks to open: as the person types (
details.reason'input-change') or presses an arrow key ('list-navigation'). onClose(event: Event, reason: Autocomplete.Root.ChangeEventReason, details: Autocomplete.Root.OpenChangeEventDetails) => void, default —- Called when the list asks to close:
reasonis'escape-key','outside-press','focus-out','item-press','input-clear','close-press', or'none'for Enter with no row highlighted;details.cancel()keeps it open. onItemSelect(event: React.MouseEvent<HTMLDivElement>, item: SearchFieldItem) => void, default —- Called when the person chooses a suggestion, with a press or with Enter on the highlighted row, with the item itself.
refRef<HTMLDivElement>, default —- Reaches the root, the
Field.Root<div>. clearLabelstring, default The locale's- Overrides "Clear", the clear button's name.
noMatchesLabelstring, default The locale's- Overrides "No matches", the message the list shows and announces when no suggestion matches.
slotProps{ label?: FieldLabelProps | undefined; inputGroup?: AutocompleteInputGroupProps | undefined; input?: ComboboxInputProps | undefined; clear?: Omit<ComboboxClearProps, "disabled"> | undefined; portal?: ComboboxPortalProps | undefined; positioner?: ComboboxPositionerProps | undefined; popup?: ComboboxPopupProps | undefined; empty?: ComboboxEmptyProps | undefined; list?: ComboboxListProps | undefined; item?: AutocompleteItemProps | undefined; }, default —- Props for each part:
label,inputGroup,input,clear,portal,positioner,popup,empty,list,item.
Use it
import { SearchField } from '@mylder/perron'Ask your agent
get_reference({ library: "base-ui", component: "Field" })get_reference({ library: "base-ui", component: "Autocomplete" })Keyboard and ARIA
From the Base UI reference for Field and Autocomplete, and the installed AriaCombobox, ComboboxInput, ComboboxClear, ComboboxList and ComboboxEmpty that Autocomplete re-exports. "From" says who does it.
| Key | Does | From |
|---|---|---|
| Typing | Changes the words (onChange, 'input-change'), narrows the rows, and opens the list once the words are not blank; emptying the box closes it ('input-clear') | Base UI |
ArrowDown, ArrowUp | Open the list when it is closed (onOpen, 'list-navigation'); move the highlight to the next or previous row, from the last row back to the input and round again; focus stays in the input | Base UI |
Enter | On a highlighted row: chooses it, filling the box with its label (onChange, 'item-press'), calling onItemSelect and closing the list. With no row highlighted: closes the list (onClose, 'none') and submits a form round the field | Base UI |
Escape | With the list open: closes it (onClose, 'escape-key'). With it closed: empties the box (onChange, '', 'escape-key') | Base UI |
Home, End | Move the caret to the start or end of the words | Base UI |
Tab, Shift+Tab | Move focus out of the field, closing the list (onClose, 'focus-out'); the clear button is not a tab stop, since Esc empties the box from the keyboard | Base UI |
A screen reader's activation of a Dismiss control | Closes the list (onClose, 'close-press'); neither control is a tab stop | Base UI |
| Element | Role | Accessible name from | States announced |
|---|---|---|---|
label | none: a native <label>, or a hidden span | Its text, the caller's label | aria-hidden="true" while the list is open, from Base UI: a known exception (below) |
inputGroup | group, from Base UI | none | none |
input | combobox, from Base UI | aria-labelledby the label | aria-expanded, aria-autocomplete="list", aria-activedescendant the highlighted row, disabled |
clear | button, native | The owned "Clear", or clearLabel, in the CloseButton's VisuallyHidden span | aria-hidden="true" while the list is open, from Base UI: a known exception (below) |
list | listbox, from Base UI | aria-labelledby the label, which Perron sets | none |
item | option, from Base UI | Its text: the item's label, then its description, and the text of its startIcon | Highlighted through the input's aria-activedescendant |
empty | status, from Base UI | none: its text, "No matches", announced politely when it appears | none |
positioner, popup | presentation, from Base UI | none | none |
Base UI's Dismiss controls | button, from Base UI: two screen-reader-only <span>s while the list is open, one before the input and one after the popup | aria-label="Dismiss", Base UI's own English: a known exception (below) | none |
Base UI's known exceptions. While the list is open, Base UI 1.9.0 adds two <span role="button" aria-label="Dismiss"> controls, from ComboboxInput and ComboboxPopup, that a screen reader can activate to close the list, named in hard-coded English that no owned string or *Label prop reaches; and its modal focus manager sets aria-hidden on everything outside the input, the popup and those controls, the label and the clear button included, until the list closes. SearchField keeps Base UI's behaviour and neither hides nor relabels them: the input stays named by the label through aria-labelledby, which reads hidden text; Esc and the arrow keys stay the field's keys; and both controls sit outside the popup, so the open list's owned-strings check never sees them. The Open story asserts them, and a Direction finding asks Base UI for a label prop.
What axe won't catch, from the contract, each with how this component meets it:
- Everything the eye is told, the ear is told, for every shape each prop accepts: the search icon is
aria-hiddenbeside a field the label names; the clear button'saria-hiddencross has the owned "Clear" as its text; a hidden label is still the input's and the list's name; "No matches" is announced as it appears; a row reads its label, its description and its start icon's own text, as a StationIcon or a LineBadge gives one. Base UI's known exceptions, which theOpenstory asserts: while the list is open, its modal focus manager hides the label and the clear button from the ear, the input keeping its name, and its twoDismisscontrols are heard in its own English. - Every landmark and named region has an accessible name: none here; SearchField is not a landmark, and a caller puts it in a
<search>element when it is the screen's search (see Do and don't). - Every default label can be overridden:
clearLabelandnoMatchesLabel. Base UI's twoDismisscontrols are its known exception: no prop reaches their hard-coded English. - Every shape the type accepts renders, in every variant:
label, an item'sdescriptionand itsstartIconrender a string, an element and a list alike; a long Danish compound wraps in the label, the rows and the empty message (CR-11). - A hidden copy kept for animation is
aria-hidden, and a collapsed region isinert: none here; the closed list renders nothing, and the clear button is unmounted while the box is empty. - Native semantics first: a native
<label>and<input>, a native<button>for the clear button, and Base UI'scombobox,listbox,optionandstatusroles; Perron adds only the listbox'saria-labelledby. Base UI'sDismisscontrols are<span role="button">s, its known exception. - Focus moves in reading order and returns on close: the label, then the input, are the field's one tab stop; the highlight in the list is virtual, so focus never leaves the input, and closing the list, choosing a row or pressing the clear button leaves it there.
- Every target is at least
size.target-minsquare: the clear button,size.control(34 px) square; a row, at leastsize.target-mintall and as wide as the list; the input,size.inputtall. Base UI'sDismisscontrols are screen-reader-only, with no box to press, and take no tab stop. - Loops stop when the system asks for less motion: none here; SearchField has no loop and no transition (CR-12).
Owned strings
| Key | English | Override | Seen or heard where |
|---|---|---|---|
clear | "Clear" | clearLabel | clear, heard as the clear button's name from the CloseButton's VisuallyHidden span; never seen |
noMatches | "No matches" | noMatchesLabel | empty, seen in the list when no suggestion matches the words, and announced politely by its status region |
clear is the key packages/perron/src/locale/en.ts already has, "A button that clears a field or a filter". noMatches is new.
The new key is noMatches, "No matches", described in en.ts as "A search field's list: shown and announced when no suggestion matches the words typed", with the Danish, Swedish and Norwegian written by the build. It says what happened in plain words and no more than the data knows, in sentence case, as Content design asks; it doesn't address the person, since a hint such as "Try a line number such as 5C." is the caller's, in the placeholder; and it stays apart from noData, "No data to show", which a data view says when its data came back empty.
Every other word is the caller's, through label, placeholder and items, but Base UI's Dismiss, the name of its two screen-reader-only controls while the list is open: its hard-coded English, a known exception (see Keyboard and ARIA).
Do and don't
For a caller using the component, with the reason. Each row is a rule: a must row's Don't is never right, a should row's needs a reason, and Checked by says what catches the Don't in a caller's code.
| Rule | Level | Do | Don't | Checked by |
|---|---|---|---|---|
| DD-1 | must | Give a label that names what is searched, "Search lines and stops", and hide it with hiddenLabel only where the icon says enough | Leave the placeholder as the field's only words: it vanishes as the person types, and it is not a name | review |
| DD-2 | must | Put a hint in the placeholder as an instruction with an example, "Line or stop, such as 5C" | Put anything the person must remember in the placeholder: it is gone once they type, and drawn in color.text.tertiary, below text contrast | review |
| DD-3 | must | Act on onItemSelect, which gives the item, to move the map to the stop or show the line | Find the chosen item from onChange's words with 'item-press': two stops can share a name | review |
| DD-4 | must | Pass filter={null} when the suggestions come already matched, such as from a server | Leave Base UI's filter on server results: it hides a match whose label doesn't contain the words, such as a stop found by its old name | review |
| DD-5 | must | Give a short label and a description that tells two rows apart, "Bus, Husum Torv to Lufthavnen" | Put a link, a button or a field in a row: an option is chosen as a whole, and the keys never reach a control inside it | review |
| DD-6 | must | Use a StationIcon or a LineBadge as an item's startIcon, which carries its own text | Use colour alone to tell a line from a stop: the mark needs its words, and the row its description | review |
| DD-7 | should | Put the field in a <search> element, or a <form role="search">, when it is the screen's search | Expect SearchField to be a landmark: it is one field, and a screen may hold more than one | review |
| DD-8 | should | Set the width on the container, or through className on the root | Give the input or the box a width through slotProps: the list takes the box's width, and the label the root's | review |
| DD-9 | should | Keep the field in a PerronProvider with the screen's locale, so "Clear" and "No matches" speak it | Translate them with clearLabel and noMatchesLabel on every instance: those are for one instance's own words | review |