Components · Structure

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.

Stable · v1.0 shadcn/ui WCAG 2.2 AA

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

Tabs with pending counts

12-lead ECG

Critical
STEMI pattern — anterior V2–V4. ST elevation 3.1 mm.
HARLAND, A. · acquired 14:26:38 · Bay 4 cart
ElementRule
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, and what they are. This is what makes hiding acceptable — without it, an empty-looking tab and a full one are identical. It is a Badge, not a number: “Troponin 2” announces a quantity with no unit, and 2 results, 2 prior studies and 2 unread messages are different facts. Severity never colours it — the alarm marks the tab.
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

The governing rule

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.

Alarm arising in a hidden panel

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.

Structure

Do

Three peer views of one patient. Counts make the hidden content countable.

Don't

A sequence, an unrelated destination and a support page in one strip. None of these are peer views of a subject.

Content behind a tab is still content in the record

A clinician who wants to know whether a patient is on amiodarone presses Ctrl + F and types the word. If the answer is in a panel they have not opened, the browser reports no match — and no match is not the same answer as no amiodarone.

The rule

An unselected panel is hidden from view, never hidden from search. Content that exists in the record and cannot be found by looking for it is the failure this component is most likely to cause, because concealment is what a tab is for.

The platform has the mechanism. hidden="until-found" keeps a panel invisible and out of the accessibility tree while leaving its text reachable by the browser's own find; when a match is inside, the browser fires beforematch and reveals it. The component listens for that and selects the matching tab, so the strip and the panel agree — otherwise the reader ends up reading a panel whose tab still says it is not selected.

Do

Ctrl + F “amiodarone” → the Medications tab opens with the match in view.

The record answered the question that was asked of it. The strip moved to match what the reader is now looking at.

Don't

Ctrl + F “amiodarone” → 0/0

The drug is in the record and the search says it is not. A reader who trusts the search now believes something false about the patient.

Arrowing through a tab is not reading it

Arrow keys move focus only; Enter, Space or a click selects. ARIA’s authoring practices permit either, and this component selected on arrow for a long time while the accessibility notes below said it did not — the per-bullet audit of the safety notes was what caught it. Automatic activation walks a keyboard user through every panel between the first tab and the fourth, rendering, announcing and loading each in passing. Manual activation means a tab is selected because someone chose it. What follows is about that selection.

What the component enforces, and what it only asks for

The tab strip holds its keyboard and its concealment rules. What it cannot hold is the decision about what is safe to conceal at all, which is the subject of most of this page.

RuleHeld by How
One tab stop, with arrow keys insideThe component A roving tabIndex with Left, Right, Home and End.
Asserted: Tabs: arrow keys move focus only, and Enter selects
A closed panel is findable by Ctrl + FThe component hidden="until-found", and beforematch selects the tab so the strip agrees with what was revealed.
Asserted: Tabs: an unselected panel is hidden until found, not just hidden
A count on a tab says what it countsThe component The count is a Badge, so a bare number with no noun cannot be rendered.
Asserted: Tabs: a count on a tab says what it counts
Selection is not carried by colour aloneThe component A 2 px underline and a weight change, plus aria-selected.
Asserted: Tabs: selection is an underline and a weight, not colour alone
An alarm is never concealed by a tabYou The component renders the panels it is given. Hoisting an alarm out of a panel into the docked region happens in your layout — nothing here can detect that you did not.
Arrowing along the strip selects nothingThe component Arrow keys, Home and End move focus; onChange fires on Enter, Space or a click. The tab stop follows focus inside the strip and returns to the selected tab on leaving it. Driven in a browser.
Asserted: Tabs: arrow keys move focus only, and Enter selects
Nothing is recorded when a tab is selectedYou onChange is a selection, not a reading. If you file a view, acknowledge a result or start a billable action there, a tab chosen and left in a second files one of them.
Two to five tabs, and never nestedYou tabs is an array of any length, and nothing stops a panel containing another Tabs.
Panels keep their state across a switchYou Panel content is yours; the component only hides and shows it.
Why this is written down

A rule stated on a component page reads as a rule the component keeps. 4 of these 8 are; 4 are obligations this page was quietly handing to the reader. The rule this page is named for — that an alarm never waits inside a hidden panel — is the one the component cannot keep.

Do's and don'ts

Do

Counts make the hidden content countable, which is what buys the right to hide anything.

Don't

No counts. A tab with two unreviewed results looks exactly like an empty one.

Accessibility

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.

Clinical safety notes

Risk controls this page specifies

Trace these in your risk file (ISO 14971) and usability engineering file (IEC 62366-1). Which of these the component keeps and which are yours is stated in the guarantees table; a control the component does not keep is one your risk file must show you keep.

Implementation

$ npx shadcn@latest add https://md.notjustany.tech/r/tabs.json
<Tabs
  label="Patient record"
  activeId={active}
  onChange={setActive}
  tabs={[
    { id: "ecg", label: "ECG", content: <p>12-lead, acquired 14:26</p> },
    { id: "troponin", label: "Troponin",
      count: { value: 2, of: { one: "result", other: "results" } },
      content: <p>47 ng/L, rising</p> },
  ]}
/>

// The count is a Badge, so a bare number with no unit cannot be shown.
// Arrow keys move focus; Enter or Space selects. onChange is a selection, not a view.

What the component takes. This table used to list defaultValue, syncToUrl, activationMode, count, keepMounted — props the component does not have, and never had. Found by the registry gate’s props-table rule.

PropTypeDefaultNotes
labelstring— Required. The tab list’s accessible name.
tabsTab[]— Required. Each an id, a label, an optional count with its noun, and the panel content.
activeIdstring— Required. Which tab is selected. Controlled.
onChange(id: string) => void— Required. Called on Enter, Space or a click — not on arrowing past.
NotJustAnyMed.Tech Design System · Tabs · v1.0 · draft for review
Reference applications named in this system are fictional; all patient data shown is fabricated.