Tabs
Peer views of one subject, only one visible at a time. Tabs are the only structural component whose entire purpose is to hide things — which is why their rules are about what may never be hidden.
Overview
Tabs solve a real problem — a subject with several equally valid views that will not fit on one screen. They solve it by concealment, and concealment in clinical software is the mechanism behind most of the failure modes elsewhere in this system: the filtered-out patient, the collapsed queue, the value below the fold.
So the component is permitted, narrowly, with one governing rule: nothing a clinician needs in order to notice something may live behind a tab.
Peer views
Tabs hold alternative views of one subject, never steps in a sequence and never unrelated destinations. If the tabs are a workflow, it is a wizard; if they are places, it is navigation.
Nothing urgent hidden
An active alarm is never behind a tab. Where a hidden panel holds something waiting, the tab says how much.
Addressable
The selected tab is in the URL. A clinician can send a colleague the view they are looking at, and the browser's back button does what it says.
Anatomy
12-lead ECG
Critical| Element | Rule |
|---|---|
| Tab label | Names the content, not the category. "Troponin", not "Labs". One or two words, never truncated. |
| Count | How many items are waiting in that panel. This is what makes hiding acceptable — without it, an empty-looking tab and a full one are identical. |
| Selected indicator | 2 px --primary underline and full-contrast text. Never colour
alone. |
| Panel | One subject's alternative view. Never a different patient, never a different encounter. |
What may never be behind a tab
An active Critical or Urgent alarm is never concealed by a tab. If a condition arises in a hidden panel, the alarm renders in the docked region above the tabs, where it is visible regardless of which tab is selected — see Alert Banner. The tab additionally marks itself, so the clinician knows where to go, but the alarm does not wait there.
The alarm is docked above and fully readable without changing tab. The Troponin tab marks itself so the clinician knows where the evidence is — it does not have to be found.
- Never hide an alarm. As above.
- Never hide patient identity. The patient header sits above the tab strip, never inside a panel.
- Never hide the primary action of the screen behind a non-default tab.
- Never hide a required form field. A clinician cannot submit a form whose errors are in a panel they cannot see; if a form needs tabs, it needs to be a sequence instead.
Structure
- Two to five tabs. One tab is not a tab. Beyond five, the labels shrink or scroll and the set stops being scannable — that content wants navigation, not tabs.
- Never nest tabs. Tabs inside tabs cannot be held under interruption and cannot be described unambiguously by assistive technology.
- The default tab is the one the screen exists for, and it is stable — never "whichever was open last time", which produces a colleague's view on your screen.
- Overflow scrolls, it does not collapse into a menu. A tab hidden inside an overflow menu is hidden twice.
- Panels keep their state when a clinician switches away and back — an annotation toggled on in the ECG view is still on when they return.
Three peer views of one patient. Counts make the hidden content countable.
A sequence, an unrelated destination and a support page in one strip. None of these are peer views of a subject.
Do's and don'ts
Counts make the hidden content countable, which is what buys the right to hide anything.
No counts. A tab with two unreviewed results looks exactly like an empty one.
Accessibility
- Full ARIA tabs pattern —
role="tablist"with an accessible name,role="tab"witharia-selected, androle="tabpanel"associated back to its tab. - Roving tabindex. One tab stop for the strip; arrow keys move between tabs, Home and End jump to the ends.
- Manual activation. Arrow keys move focus; Enter or Space selects. Auto-activation on arrow renders every panel in passing, which is noisy for screen-reader users and expensive when a panel loads data.
- Selected state is not colour alone — underline plus text weight plus
aria-selected. - Counts are in the tab's accessible name: "Troponin, 2 items" rather than a visual badge a screen reader announces as a bare number.
- Panels are labelled by their tab and are focusable so keyboard users land in the content after selecting.
- At 320 px and 200 % zoom the strip scrolls horizontally with visible affordance; labels never truncate.
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 — nothing needed goes unnoticed because it was in another panel. Counts and docked alarms are what buy the right to hide anything at all.
- Efficiency — one subject's views reachable without losing context or re-establishing identity, which is what tabs are genuinely good at.
- Satisfaction — confidence that the screen is not concealing something. A clinician who checks every tab before acting has been given a strip that does not tell them enough.
Clinical safety notes
Trace these in your risk file (ISO 14971) and usability engineering file (IEC 62366-1).
- Alarms never behind a tab. Mitigates: a Critical condition arising in a hidden panel and waiting there unseen.
- Counts on tabs with pending content. Mitigates: an unattended result being indistinguishable from an empty view.
- Patient identity above the strip. Mitigates: acting inside a panel with the record's identity out of view.
- Stable default tab, never last-used. Mitigates: inheriting a colleague's view and assuming it is the standard one.
- No nested tabs; five maximum. Mitigates: structure that cannot be held under interruption.
- Panels retain state. Mitigates: silently reverting a deliberate view setting such as annotation or calibration.
- Manual activation. Mitigates: loading and announcing panels a clinician only passed through.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/tabs.json
<Tabs defaultValue="ecg" syncToUrl activationMode="manual">
<TabsList aria-label="Patient record">
<TabsTrigger value="ecg">ECG</TabsTrigger>
<TabsTrigger value="troponin" count={2}>Troponin</TabsTrigger>
<TabsTrigger value="prior">Prior studies</TabsTrigger>
</TabsList>
<TabsContent value="ecg" keepMounted>…</TabsContent>
</Tabs>
// Alarms are hoisted out of panels into the docked region, so they are
// visible whichever tab is selected. Enforced, not left to the call site.
if (panelContainsAlarm(child) && !hoisted) {
throw new Error(
"[Tabs] an AlertBanner at critical/urgent cannot render inside a TabsContent. " +
"Hoist it to the docked alert region and mark the trigger instead."
)
}
| Prop | Type | Default | Notes |
|---|---|---|---|
defaultValue | string | — | Required. Stable; never restored from previous session. |
syncToUrl | boolean | true |
Selected tab is addressable and back-navigable. |
activationMode | manual | automatic |
manual | Automatic warns for panels that fetch. |
count | number | — | Included in the accessible name, not just rendered. |
keepMounted | boolean | true |
Panels retain view state across switches. |
Related
- Alert Banner — where hoisted alarms render.
- Card — the usual content of a panel.
- Patient header — sits above the strip.
- Data grid — the same disclosure logic for hidden rows.