Search
Find content by typing. The only search field in the system.
Basic
A live inline filter: results update as the user types, with no submit step. Use it where the results are cheap to recompute and visible without scrolling.
<div class="sr-search">
<label class="sr-visually-hidden" for="q">Search patients</label>
<div class="sr-search__row">
<div class="sr-search__control">
<div class="sr-search__field">
<span class="sr-search__icon"><!-- nav/search --></span>
<input type="search" class="sr-search__control-input" id="q"
placeholder="Search patients" autocomplete="off">
<button type="button" class="sr-search__clear" aria-label="Clear search">
<!-- nav/clear -->
</button>
</div>
</div>
<button type="submit" class="sr-search__submit" disabled>Search</button>
</div>
</div>
<!-- Typeahead adds role="combobox" + a role="listbox" popover. The arrow keys,
Home/End, Enter and Esc are yours to wire up. -->
With a button
For a query that costs something — a backend call, a cross-organisation lookup. The button is disabled until there is a query, so an empty search cannot be sent. Type into the field and watch it enable.
<div class="sr-search">
<label class="sr-visually-hidden" for="q">Search patients</label>
<div class="sr-search__row">
<div class="sr-search__control">
<div class="sr-search__field">
<span class="sr-search__icon"><!-- nav/search --></span>
<input type="search" class="sr-search__control-input" id="q"
placeholder="Search patients" autocomplete="off">
<button type="button" class="sr-search__clear" aria-label="Clear search">
<!-- nav/clear -->
</button>
</div>
</div>
<button type="submit" class="sr-search__submit" disabled>Search</button>
</div>
</div>
<!-- Typeahead adds role="combobox" + a role="listbox" popover. The arrow keys,
Home/End, Enter and Esc are yours to wire up. -->
With an icon button
The same control where a text button will not fit — mobile, dense toolbars. The button is 40×40, square to the field and comfortably over the 24×24 minimum in WCAG 2.2. Use it because space demands it, not by preference: the word "Search" is easier to read at a glance than a glyph.
<div class="sr-search">
<label class="sr-visually-hidden" for="q">Search patients</label>
<div class="sr-search__row">
<div class="sr-search__control">
<div class="sr-search__field">
<span class="sr-search__icon"><!-- nav/search --></span>
<input type="search" class="sr-search__control-input" id="q"
placeholder="Search patients" autocomplete="off">
<button type="button" class="sr-search__clear" aria-label="Clear search">
<!-- nav/clear -->
</button>
</div>
</div>
<button type="submit" class="sr-search__submit" disabled>Search</button>
</div>
</div>
<!-- Typeahead adds role="combobox" + a role="listbox" popover. The arrow keys,
Home/End, Enter and Esc are yours to wire up. -->
Typeahead
Type to look something up, then pick a result. Focus the field below and use ↓ ↑ Home End Enter Esc — the whole model works without the mouse. The matched run is bold, and the second line disambiguates results that would otherwise read alike.
<div class="sr-search">
<label class="sr-visually-hidden" for="q">Search patients</label>
<div class="sr-search__row">
<div class="sr-search__control">
<div class="sr-search__field">
<span class="sr-search__icon"><!-- nav/search --></span>
<input type="search" class="sr-search__control-input" id="q"
placeholder="Search patients" autocomplete="off">
<button type="button" class="sr-search__clear" aria-label="Clear search">
<!-- nav/clear -->
</button>
</div>
</div>
<button type="submit" class="sr-search__submit" disabled>Search</button>
</div>
</div>
<!-- Typeahead adds role="combobox" + a role="listbox" popover. The arrow keys,
Home/End, Enter and Esc are yours to wire up. -->
No results
The failed query is quoted back, so the user can see what was actually tried. It is usually a typo they spot instantly.
<div class="sr-search">
<label class="sr-visually-hidden" for="q">Search patients</label>
<div class="sr-search__row">
<div class="sr-search__control">
<div class="sr-search__field">
<span class="sr-search__icon"><!-- nav/search --></span>
<input type="search" class="sr-search__control-input" id="q"
placeholder="Search patients" autocomplete="off">
<button type="button" class="sr-search__clear" aria-label="Clear search">
<!-- nav/clear -->
</button>
</div>
</div>
<button type="submit" class="sr-search__submit" disabled>Search</button>
</div>
</div>
<!-- Typeahead adds role="combobox" + a role="listbox" popover. The arrow keys,
Home/End, Enter and Esc are yours to wire up. -->
States
Filled shows the clear affordance; loading replaces it with a spinner and announces "Searching…" to assistive technology; error carries an icon and a message, never the red border alone.
As a labelled form field
Search carries its own Label, Hint and Required properties.
Do not wrap an Input around it — the Input set's Type=Search variants were removed on
4 June 2026, and Search is now the only search field in the system.
Source for both the Figma "Guidelines / Usage notes" panel and the design system website page. Do not fork this content between the two.
Figma: Search (1715:375), Search Suggestions (1716:238) on page 1701:17851 Code: packages/web/src/search/search.css, @dhcw/sr-react → Search
When to use
Use Search when the user finds content by typing, and the result is a list or view that changes because of what they typed.
| You want | Use |
|---|---|
| Filter a table, list or panel as the user types | Search, Type=Basic |
| A query sent to a service, patient lookup, document search | Search, Type=With Button |
| The same, where horizontal space is tight (mobile, toolbars) | Search, Type=With Icon Button |
| Type to look up a person, place or coded term, then pick a result | Search, Type=Typeahead |
| Pick one value from a known set, typing to narrow it | Autocomplete |
| Pick one value from a short fixed set | Select |
| Free text that is not a query | Input |
| Perform an action | Button |
Search is the only search field in the system. The Input set's Type=Search variants were removed on 2026-06-04. To put a search inside a labelled form field, turn on Label / Hint / Required on a Search instance, do not wrap an Input around it.
Choosing a type
- Basic fires live. Only use it where results are cheap enough to update on every keystroke, and where the user can see the effect of typing without scrolling.
- With Button is for a query that costs something, a backend call, a cross-organisation lookup. The button is disabled until there is a query, so an empty search cannot be sent.
- With Icon Button is the same control at 40×40. Use it where a text button will not fit, not as a general preference: an icon-only button is harder to read at a glance than the word "Search".
- Typeahead pairs the field with the suggestions popover. Use it when the user is looking for a thing, a clinician, a ward, a coded term, rather than filtering something already on screen.
Content
- The placeholder says what the field searches, not how to use it. "Search patients", not "Type here".
- For With Button / With Icon Button, name the accepted input: "Enter NHS number or name…".
- "No results" quotes the query back,
No matches for "cardew", so the user can see what was actually tried. It is usually a typo they spot instantly. - Suggestion sub-text disambiguates (specialty, location, code system). Never put information the user needs to make a safe choice there alone.
- Every field has a real label, even standalone search bars.
hideLabelmoves it off-screen; it does not remove it.
Accessibility
- Typeahead follows the WAI-ARIA combobox pattern. The input is
role="combobox"witharia-expanded,aria-controlsandaria-activedescendant; the popover isrole="listbox"withrole="option"rows. - Keyboard: ↓ ↑ move the active row, Home / End jump to the first and last, Enter selects, Esc closes the popover and leaves focus in the input.
- Esc does not clear the query. Losing a long query to a stray Esc is worse than the list staying open.
- Clear returns focus to the input. Clearing is a step in searching, not the end of it.
- The clear button's accessible name is
Clear search; the icon button's isSearch. - Loading is announced, not only shown:
Searching…in a live region alongside the spinner. - Error is carried by an icon and a message, not the red border alone. The message is linked with
aria-describedbyand the input getsaria-invalid. - The matched run in a suggestion is bold and carries the option's semantics, the highlight is never the only cue.
- Focus is a 3px
Border/Focusring drawn outside the 1px border, so nothing in the row moves when focus lands. - The icon button is 40×40, above the 24×24 minimum of WCAG 2.2 SC 2.5.8 (AA).
Engineering
- Debounce typeahead queries at 150–250ms and cancel in-flight requests on a new keystroke, or results arrive out of order and the user sees the answer to a query they have already moved past.
- Enforce minimum query length at the API boundary and surface it as the Error state: "Enter at least 2 characters".
- Pass
filter={false}in React when the server has already filtered, the default filters locally, which would filter the results twice. @dhcw/sr-react→Searchimplements the whole keyboard model. If you hand-roll the markup, port it too; it is the half people leave out.
Do / Don't
- Do keep the placeholder describing the scope of the search.
- Do use Typeahead where the user knows roughly what they want but not how it is spelled.
- Don't use Basic against an expensive backend, that is With Button.
- Don't use Search to pick from a short known list; that is Select.
- Don't rely on the suggestion sub-text to carry clinical meaning.
- Don't clear the query on Esc.
Known gaps
- Radius. Figma draws 4px;
--radius-smis 2px. The code follows the token, as Input and Select already do. This is a system-wide mismatch, not a Search one. - Suggestion row padding is 8/12 in code against Figma's 10/14, 10 and 14 are off the 4px spacing grid, and 12 matches the Select option's left inset so the two lists align in the same form.
- The MAUI implementation does not exist yet. There is no native combobox; composing
Entry+CollectionViewand hand-wiring the semantics is the route, and it needs a decision before it is built.
Accessibility requirements
| Requirement | WCAG SC | How Single Record meets it | Test method |
|---|---|---|---|
| The field has a name | 1.3.1, 4.1.2 | Every field has a real label. A standalone search bar hides it with .sr-visually-hidden, which removes the visual footprint and nothing else. | Screen reader announce |
| Suggestions follow the combobox pattern | 4.1.2 | role="combobox" with aria-expanded, aria-controls and aria-activedescendant; the popover is role="listbox" with role="option" rows. | Screen reader, arrow through the list |
| The whole list is reachable by keyboard | 2.1.1 | ↓ ↑ move the active row, Home/End jump to the ends, Enter selects, Esc closes. Esc does not clear the query. | Keyboard only |
| Focus is visible and does not move the layout | 1.4.11, 2.4.7 | A 3px Border/Focus ring outside the 1px border, so nothing in the row shifts when focus lands. | Keyboard tab |
| The matched run is not colour alone | 1.4.1 | The match is bolded, and the row carries aria-selected and aria-activedescendant regardless of how it looks. | Greyscale review |
| Loading is announced | 4.1.3 | A role="status" live region says "Searching…" alongside the spinner, so the wait is not visual only. | Screen reader, slow network |
| Errors are exposed and actionable | 3.3.1, 3.3.3 | aria-invalid on the input, the message linked by aria-describedby, and an icon beside it so the error is not the border colour. | Screen reader, submit short query |
| Controls meet the target size | 2.5.8 | The icon button is 40×40 and the field is 40px tall. The clear button is 24×24, the minimum, and sits inside a 40px row. | Measure |
| Clear does not strand focus | 2.4.3 | Clearing empties the field and returns focus to it, rather than leaving focus on a button that has just disappeared. | Keyboard only |
| Text resizes to 200% | 1.4.4 | The field and popover size from their content; suggestion rows wrap onto two lines rather than truncating. | Browser zoom to 200% |