Select
Choosing one value from a known set. The component's defining risk is not how it opens — it is what it says before anyone has chosen.
Overview
A select looks like a low-stakes control and behaves like a high-stakes one, because it always shows something. Whatever is in the closed trigger reads as an answer, and if the form is submitted without the clinician ever opening it, that answer is recorded as theirs.
Everything below follows from that: an unanswered select must look unanswered, options must be readable in full, and no clinical value is ever pre-chosen.
Unanswered by default
An untouched select shows a placeholder, not a value. Nothing clinical is chosen on a clinician's behalf.
Fully legible
Every option is readable in full before it is chosen. An option a clinician has to guess at is a choice they did not really make.
Reversible
Changing a selection is as easy as making one, and never destroys anything else on the form.
Anatomy
- Non-ischaemic ST elevation
- Early repolarisation
- Pericarditis
- Left ventricular hypertrophy
- Signal
- Artefact
- Lead misplacement
| Element | Rule |
|---|---|
| Label | Always a real <label for> outside the control. Same rule as
Text field. |
| Trigger | 44 px tall, --input border at ≥ 3:1. Placeholder text is visibly
distinct from a chosen value, and not by colour alone. |
| Group headings | Real grouping semantics, never a disabled option used as a divider. |
| Options | Full text, wrapping if necessary. Never truncated. |
A select showing the first option by default records that option every time a clinician does not notice the field. Anything with clinical meaning — a reason, a severity, a route, a site, a dose band — opens on a placeholder and stays invalid until chosen. The exception is narrow: a value the system genuinely knows, such as the acquiring device or the current user's unit, which is pre-filled and clearly marked as such.
Options
- Never truncate. "Left ventricular hypertroph…" and "Left ventricular hypertrophy with strain" are different findings. Options wrap; the list widens; the label gets shorter. Nothing gets an ellipsis.
- Order by clinical likelihood, not alphabetically, where a defensible ordering exists — and state the ordering in the hint if it is not obvious.
- Group when there are more than about seven, using real group semantics.
- Add search above about fifteen. Below that, search costs more than it saves.
- Include an explicit "Other" with free text wherever the list could be incomplete — a clinician forced into the nearest wrong option produces worse data than one who can type.
- Never disable options without saying why. Same rule as Interaction states.
Native or custom
| Use | When | Why |
|---|---|---|
Native <select> |
Simple lists, and all touch platforms | The OS picker is larger, better at one-handed use, works gloved, and needs no custom keyboard handling. It is almost always the safer control on a cart. |
| Custom listbox | Grouping, search, rich option content, multi-select | Only where native cannot express it. Accepts the full cost of implementing keyboard, focus and announcement correctly. |
A custom select that is merely prettier than the native one is not worth its accessibility surface. Reach for it when the requirement genuinely exceeds what native offers.
States and validation
- Unanswered is a state, not an error — until submission is attempted. Then it becomes an error naming the field and moving focus to it.
- Changing a selection never clears other fields. Where a change genuinely invalidates dependent input, say so and confirm before discarding anything typed.
- Loading options is visible. An empty list while a fetch is in flight is indistinguishable from a list with no options; show the pending state and, on failure, say the options could not be loaded rather than presenting an empty set.
- Read-only shows the chosen value at full contrast with no caret — it is information, not an unavailable control.
Unanswered looks unanswered. Nothing is recorded unless a clinician chooses it.
First option pre-selected. Every unattended form now records "Critical", and nobody chose it.
Do's and don'ts
Unanswered looks unanswered. Nothing is recorded unless a clinician chooses it.
First option pre-selected. Every unattended form records “Critical”, chosen by nobody.
- Left ventricular hypertrophy
- Left ventricular hypertrophy with strain
- Other…
Full option text, wrapping if needed, plus an explicit “Other” where the list may be incomplete.
Truncated to the same visible string. Two different findings, indistinguishable at the point of choosing.
Accessibility
- Native first. The most accessible select is the one the platform provides.
- Custom listboxes implement the full pattern —
role="combobox"on the trigger witharia-expanded,role="listbox"androle="option"witharia-selected, arrow-key navigation, Home/End, type-ahead, Esc to close, and focus returning to the trigger. - Group headings use real semantics, so screen-reader users hear the grouping rather than a stray option.
- The listbox is reachable at 200 % zoom and repositions inside the viewport instead of clipping.
- Selection is not colour alone — the selected option carries a check as well as a fill.
- Touch targets: options obey the physical floor from Scaling, which usually means fewer visible at once on a cart. That is the correct trade.
Outcomes of use
What this contributes to, in the terms of Usability & context of use. These are attributes believed to contribute to an outcome; the outcome itself is settled by observing real use in a specified context, not by this page.
- Effectiveness — the recorded value is one the clinician actually chose. Silent defaults are the failure mode this component owns.
- Efficiency — the right option found without reading the whole list; clinical ordering and grouping do more for this than search does.
- Satisfaction — confidence that the form says what they meant, and no sense of having been answered on their behalf.
Clinical safety notes
Trace these in your risk file (ISO 14971) and usability engineering file (IEC 62366-1).
- No pre-selected clinical value. Mitigates: a default recorded as a clinician's decision without being seen.
- Options never truncated. Mitigates: two similar findings being indistinguishable at the point of choosing.
- "Other" with free text where the list may be incomplete. Mitigates: forced mis-classification into the nearest wrong option.
- Failed option loading is stated. Mitigates: an empty list read as "no applicable options".
- Changing a selection never silently clears input. Mitigates: loss of typed clinical text.
- Native control on touch. Mitigates: mis-selection when gloved or one-handed.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/select.json
<Select
label="Reason for override"
placeholder="Select a reason" // required — there is no default value
hint="Optional. Recorded with your assessment."
clinical // forbids defaultValue; forces native on touch
onValueChange={setReason}
>
<SelectGroup label="Non-ischaemic ST elevation">
<SelectItem value="early-repol">Early repolarisation</SelectItem>
<SelectItem value="pericarditis">Pericarditis</SelectItem>
</SelectGroup>
<SelectOther /> // "Other" + free text
</Select>
// Enforced, not documented.
if (clinical && defaultValue !== undefined) {
throw new Error(
"[Select] `clinical` selects may not carry a defaultValue. " +
"A value nobody chose must not be recorded as a decision."
)
}
| Prop | Type | Default | Notes |
|---|---|---|---|
label | string | — | Required. |
placeholder | string | — | Required. The unanswered state must read as unanswered. |
clinical | boolean | false |
Forbids defaultValue; renders native on touch platforms. |
searchable | boolean | auto above 15 options | Filters, never hides — the full list stays reachable by clearing the query. |
optionsError | string | — | Renders instead of an empty list when loading failed. |
Related
- Text field — labels, hints and validation behaviour.
- Clinician override — the reason list in context.
- Interaction states — disabled versus read-only.
- Scaling & displays — option target size.