Skip to the content

Kongens Nytorv

Metro M1, M2 and M3.

Props

NameTypeDefaultDescription
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.
hiddenLabelbooleanfalseWhether 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—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.
valuestring—The words in the box, when the caller controls them.
defaultValuestring—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.
openboolean—Whether the list of suggestions is shown, when the caller controls it.
defaultOpenbooleanfalseWhether 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.
refRef<HTMLDivElement>—Reaches the root, the Field.Root <div>.
clearLabelstringThe locale'sOverrides "Clear", the clear button's name.
noMatchesLabelstringThe locale'sOverrides "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, default 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.
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 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, default —
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.
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'). event is details.event.
openboolean, default —
Whether the list of suggestions is shown, when the caller controls it.
defaultOpenboolean, default false
Whether the list is shown at first, when open is 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: 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, 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.

KeyDoesFrom
TypingChanges 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, ArrowUpOpen 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 inputBase UI
EnterOn 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 fieldBase UI
EscapeWith the list open: closes it (onClose, 'escape-key'). With it closed: empties the box (onChange, '', 'escape-key')Base UI
Home, EndMove the caret to the start or end of the wordsBase UI
Tab, Shift+TabMove 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 keyboardBase UI
A screen reader's activation of a Dismiss controlCloses the list (onClose, 'close-press'); neither control is a tab stopBase UI
ElementRoleAccessible name fromStates announced
labelnone: a native <label>, or a hidden spanIts text, the caller's labelaria-hidden="true" while the list is open, from Base UI: a known exception (below)
inputGroupgroup, from Base UInonenone
inputcombobox, from Base UIaria-labelledby the labelaria-expanded, aria-autocomplete="list", aria-activedescendant the highlighted row, disabled
clearbutton, nativeThe owned "Clear", or clearLabel, in the CloseButton's VisuallyHidden spanaria-hidden="true" while the list is open, from Base UI: a known exception (below)
listlistbox, from Base UIaria-labelledby the label, which Perron setsnone
itemoption, from Base UIIts text: the item's label, then its description, and the text of its startIconHighlighted through the input's aria-activedescendant
emptystatus, from Base UInone: its text, "No matches", announced politely when it appearsnone
positioner, popuppresentation, from Base UInonenone
Base UI's Dismiss controlsbutton, from Base UI: two screen-reader-only <span>s while the list is open, one before the input and one after the popuparia-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-hidden beside a field the label names; the clear button's aria-hidden cross 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 the Open story 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 two Dismiss controls 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: clearLabel and noMatchesLabel. Base UI's two Dismiss controls are its known exception: no prop reaches their hard-coded English.
  • Every shape the type accepts renders, in every variant: label, an item's description and its startIcon render 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 is inert: 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's combobox, listbox, option and status roles; Perron adds only the listbox's aria-labelledby. Base UI's Dismiss controls 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-min square: the clear button, size.control (34 px) square; a row, at least size.target-min tall and as wide as the list; the input, size.input tall. Base UI's Dismiss controls 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

KeyEnglishOverrideSeen or heard where
clear"Clear"clearLabelclear, heard as the clear button's name from the CloseButton's VisuallyHidden span; never seen
noMatches"No matches"noMatchesLabelempty, 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.

RuleLevelDoDon'tChecked by
DD-1mustGive a label that names what is searched, "Search lines and stops", and hide it with hiddenLabel only where the icon says enoughLeave the placeholder as the field's only words: it vanishes as the person types, and it is not a namereview
DD-2mustPut 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 contrastreview
DD-3mustAct on onItemSelect, which gives the item, to move the map to the stop or show the lineFind the chosen item from onChange's words with 'item-press': two stops can share a namereview
DD-4mustPass filter={null} when the suggestions come already matched, such as from a serverLeave 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 namereview
DD-5mustGive 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 itreview
DD-6mustUse a StationIcon or a LineBadge as an item's startIcon, which carries its own textUse colour alone to tell a line from a stop: the mark needs its words, and the row its descriptionreview
DD-7shouldPut the field in a <search> element, or a <form role="search">, when it is the screen's searchExpect SearchField to be a landmark: it is one field, and a screen may hold more than onereview
DD-8shouldSet the width on the container, or through className on the rootGive the input or the box a width through slotProps: the list takes the box's width, and the label the root'sreview
DD-9shouldKeep the field in a PerronProvider with the screen's locale, so "Clear" and "No matches" speak itTranslate them with clearLabel and noMatchesLabel on every instance: those are for one instance's own wordsreview