Components · Structure

Accordion

Two containers with opposite promises. A layout panel says this is here, and it stays here. An accordion says this is here, but you will have to ask for it. In clinical software the second promise is expensive, so it is made rarely and never about anything that changes a decision.

Stable · v1.0 shadcn/ui WCAG 2.2 AA Merged: Accordion · Layout panel

Overview

Both exist to give a screen structure the clinician can rely on. The panel does it by drawing a boundary — everything inside belongs to one subject, and the panel header says which. The accordion does it by trading visibility for density.

That trade is the whole risk. Collapsed content is content nobody read. Not "content read later" — in practice, not read at all. So the question this page keeps asking is the same one: is it acceptable for a clinician to never open this? If the answer is no, it does not go in an accordion.

The boundary is the meaning

A panel's border is a claim about what belongs together. Nothing inside a patient panel belongs to another patient.

Collapsed means optional

Only content a clinician can safely never open is eligible for an accordion. Everything else is on screen.

The header carries the state

A closed section still reports what is inside it — the count, and any priority that would change what happens next.

Layout panel

Panel: a boundary with a stated subject
Serial troponin R. Halvorsen · MRN 55-1120
Baseline hs-cTnI
14ng/L
ref < 34 ng/L
drawn 14:41 · central lab
Repeat hs-cTnI · 1 h
47ng/L
+33 ng/L over 60 min · ref < 34 ng/L
drawn 15:41 · central lab
PartRule
Header Required. Names the content. On patient-scoped panels it also names the patient — see Patient header.
Subject One panel, one subject. A panel that mixes two patients' data has no valid header, and that is the signal it is wrong.
Border 1 px at ≥ 3:1 against both adjoining surfaces (WCAG 2.2 SC 1.4.11). The boundary is load-bearing, so it must actually be visible.
Body Scrolls internally only when the panel has a fixed height and its header stays put. Never a scroll region with no visible edge.
Toolbar Optional, inside the border — that is what makes its scope unambiguous. See Toolbar.
Elevation Panels sit flat. Elevation is reserved for things that overlay — see Elevation.

Accordion

Closed sections still report their contents

ST elevation 3.1 mm in V2–V4 measured at J+60 ms. Reciprocal depression in II, III, aVF.

AcuteLine model v4.2 · analysed 15:41:04 · decision support only

The third section is closed but declares that something advisory is inside it — a lead was reversed. A clinician who never opens it still knows to.

The rule that carries the risk

An accordion may never be the only route to an alarm, an active finding, an unacknowledged item, or anything the clinician must weigh before committing. Those are rendered expanded, or not in an accordion at all. If a section header would ever need to say "critical", the content behind it should not have been collapsed.

How to build one

This page previously documented a hand-built accordion — a button, a region, and aria-expanded — without asking whether that was the right construction. It usually is not.

The rule

Use <details> and <summary> unless the open state has to be driven by the application. The native element is keyboard-operable and correctly announced with no script, it survives a bundle that failed to load, and the browser's own find-in-page opens a closed section to reveal a match.

Native disclosure — try Ctrl+F for "reversed"
Prior recordings 3Show
14:26:38 · 09:12:04 · 12 Aug 22:40:11
Acquisition notes 1 advisory Show
Lead V3 reversed on acquisition. Corrected before analysis.

Why find-in-page is the deciding argument

A clinician who presses Ctrl+F and types troponin expects the page to answer. In a hand-built accordion the collapsed content is in the DOM but hidden, so the browser reports no match — and the reader concludes the page does not contain it. That is content which exists, is on screen in principle, and cannot be found, which is the failure this component's own rule was written to prevent. It is also invisible in testing, because the person testing knows where the content is.

Native <details> is opened by the browser when a match is inside it. If you must hand-build, the collapsed region uses hidden="until-found" rather than hidden, and listens for beforematch to set its own expanded state. A plain hidden is a defect.

ConstructionUse when Cost
<details> The default. The reader opens and closes sections and nothing else needs to know. Open state is the element's, not the application's. The disclosure marker must be removed by hand, and animating the open transition is still uneven across engines — which costs this system nothing, because it caps animation at 150 ms anyway.
Button and region The application must drive the state — restoring a saved view, or opening every section that contains a finding. You now own keyboard behaviour, announcement, and find-in-page. All three are easy to get wrong and only the third fails silently.

Never use name to group them. The attribute makes <details> elements mutually exclusive, which is exactly the exclusive-open behaviour this component forbids — the platform offers it, and we decline.

Heading level

The node lives inside a heading at the level the surrounding document implies, so an accordion under an h2 uses h3. Getting this wrong breaks heading navigation, which is the primary way a screen-reader user moves through a long record — and it is the fastest route for everyone else too. With <details>, wrap the <summary> content in the heading rather than the element itself.

Deep links and scroll

Anatomy of a node

Six parts, four of which stay readable while the node is closed. That is the whole design problem: a closed node is the only thing most readers will ever see of it, so everything needed to decide whether to open it has to survive the collapse.

PartVisible closed Rule
NodeYes The whole header is the control. Minimum 44 px, per Touch & gestures.
LabelYes A word or short phrase naming what is inside. Required.
Secondary textYes Optional. Qualifies the label — a time, a source, a scope. Never the finding itself.
MetadataYes A count and the highest priority contained, as Badge and Status chip. This is the part that makes a closed node safe.
CaretYes Decorative. aria-expanded carries the state; the caret only mirrors it.
ContentNo A region labelled by the node, so its boundary is announced on entry and exit.
There is no icon slot

Other systems put an icon on the node to hint at the content. This one carries meaning in text throughout — see Contributing — so the job an icon would do is done by the metadata: a count says how much is inside, a status chip says how urgent the worst of it is. Both are readable aloud, survive a printout, and need no comprehension study.

Variants

Three choices, each fixed for the whole accordion rather than per node.

Type

Divider and filled

DIVIDER

FILLED

Divider is the default — hairlines between nodes, no gaps, one enclosing border. It reads as one object with parts. Filled gives each node its own surface with a 4 px gap, and reads as a list of separable things. Choose by which is true. Never mix them in one accordion: a node that looks unlike its neighbours reads as a different kind of thing, and readers will look for a meaning that is not there.

Size

SizeNode height Where
Default44 px Everywhere. This is the floor from Touch & gestures, not a preference.
Compact32 px Only where there is no touch input at all — a reviewing workstation with a mouse and a keyboard. Below the floor, so it is a documented exception with a stated context, not a density setting a user may pick.

The floor exists for a gloved fingertip, which has a larger contact patch and no feel for the edge of a target. A 32 px node is not a denser layout; it is a control that cannot reliably be acquired. If a deployment is uncertain — a workstation that is sometimes a cart — it is not a no-touch context.

Node format

Label format and key–value format

The key–value format puts the value at the far end of the node, where it stays readable closed. The accordion then doubles as a summary a clinician can scan without opening anything, and the expansion carries the working — the method, the source, the interval. Values follow Key–value pair: units always present, and an absence stated in words rather than left blank.

Secondary text qualifies the label — a time, a source, a scope. It may be stacked under the label or inline beside it, consistently. It never carries the finding: a node reading "Findings · ST elevation 3.1 mm" has put a clinical conclusion in a place the reader may skip.

Configuration

Behaviour

BehaviourThis system
Opening a second node Both stay open. Exclusive-open — where opening one node closes another — is not available. Comparing two sections is an ordinary thing to want, and content vanishing from under a reader who did not ask for it is the failure this component is most prone to.
Keeping every node visible Give the node's content its own scroll rather than growing the page. The set of headers then stays on screen, which is the reason the accordion was chosen.
Inside a panel The panel does not resize as nodes open and close. A container that grows and shrinks moves everything around it, and Reprioritisation covers what that costs a reader mid-task.
Pointer The whole node is the target. Activation is on release over the target, per Touch & gestures, so a mis-landed touch can be dragged away and abandoned.
Keyboard Tab to the node, Space or Enter to toggle. No arrow-key roving — these are buttons in headings, not a composite widget, and heading navigation is the faster route.
New content arriving The node's metadata updates; the open/closed state does not. Nothing collapses because data arrived.

Formatting

States

StateRendering
Collapsed Header, count, and highest contained priority. aria-expanded="false". The caret is decorative; the state is in the attribute.
Expanded Region labelled by its header button, so its boundary is announced.
Loading a section Content loads on expand; the header shows activity and the section keeps its height stable enough not to throw the page.
Section empty Expands to a stated reason, never to nothing. See Empty state.
Section unavailable Header stays, marked unavailable with a reason. Sections never disappear — a missing section is indistinguishable from a section that never existed.
Panel degraded The panel renders with its header and an explicit statement of what is missing. It does not collapse to nothing.

What the component enforces, and what it only asks for

Built on <details>, so the platform holds the mechanics and the component holds one rule the platform has no opinion about.

RuleHeld by How
A closed section describes itselfThe component summary is required. A section collapsed to a bare title is not expressible.
Asserted: Accordion: secondary text and metadata survive the collapse
One section cannot close anotherThe component The name attribute is deliberately not plumbed through, so the platform's exclusive-accordion behaviour is unavailable.
Asserted: Accordion: no exclusive-open, so no name attribute is emitted
Collapsed content is findable by Ctrl + FThe platform A native <details> is opened by the browser to reveal a match. A hand-built accordion is not, which is why this is not one.
Keyboard operation and announcementThe platform Free, and it survives a bundle that failed to load.
Every item has the accordion’s typeThe component Type and density are read from the accordion. Each item took its own type, defaulting to divider, so <Accordion type="filled"> reached none of its items; the test that should have caught it passed the type to both.
Asserted: Accordion: the accordion's type reaches every item
Compact is refused by the type, and is actually compactThe component Compact without noTouchInput does not compile. It was a runtime throw — which takes a clinical screen down over a layout preference — guarding a density that set an undefined class and left the nodes at 44 px.
Asserted: Accordion: compact nodes are actually smaller, and only once no-touch is named
Open and closed are shown in wordsThe component “Show” or “Hide” in the header, following the element, including when find-in-page opens it. The page said this while nothing rendered it. Driven in a browser.
Asserted: Accordion: whether a section is open is shown in words
The heading level fits the documentYou headingLevel is yours to set correctly; nothing knows what surrounds it.
Nothing that must not be missed is insideYou The component hides what you give it. An unacknowledged finding in a collapsed section is concealed exactly as well as anything else.
Why this is written down

A rule stated on a component page reads as a rule the component keeps. 2 of these 6 are; 2 are obligations this page was quietly handing to the reader. The platform is doing most of the work here, which is the argument for the construction. The judgement about what may be collapsed is still yours.

Do's and don'ts

Do

Closed, but the header declares what is inside. Collapse costs a click, not a finding.

Don't

A vague label hiding an advisory. Nothing on screen gives the clinician a reason to open it.

Do
ST elevation V2–V4 · unacknowledged

An active finding is rendered, not collapsed. Nothing that changes the next action lives behind a caret.

Don't

A critical alarm collapsed behind a count. The badge is red and still nobody opens it during a busy shift.

Do
Serial troponin R. Halvorsen · MRN 55-1120
14 → 47 ng/L over 60 min

One panel, one subject, named in the header. The boundary is a claim the header can back up.

Don't
Recent results
14 → 47 ng/L · 8 → 9 ng/L · 320 → 290 ng/L

Three patients' trajectories inside one border, under a header that names none of them.

Do

ST elevation 3.1 mm, V2–V4, at J+60 ms.

14:26:38 · 09:12:04 · 12 Aug 22:40:11

Two sections open at once. Comparing detail against priors is the reason both exist.

Don't

14:26:38 · 09:12:04 · 12 Aug 22:40:11

Exclusive-open. Opening the priors closed the analysis the clinician was reading, and they have to remember what it said.

Do

One type throughout. The nodes look like members of one set, which is what they are.

Don't

Divider above, filled below. The difference looks deliberate, so a reader hunts for the meaning it carries. There isn't one.

Do

Two words, then the qualifier on its own line. Scannable down a column of eight nodes, which is how it will actually be read.

Don't

A sentence instructing the reader to do the thing the control already does. It wraps to three lines and the advisory has vanished into prose.

Do

Count, then priority, then caret — the same slots in the same order on every node. The eye locates them by position before it reads them.

Don't

The caret has moved edges and the chip has changed places. Scanning the column is now a search of each node in turn.

Do

44 px on anything a finger may reach. The default, and the only size available unless the deployment has no touch input at all.

Don't

Compact on a cart, because it fitted more in. A gloved fingertip cannot reliably acquire a 32 px node, and the neighbour it hits instead is one row away.

Do

<details> — or hidden="until-found" if hand-built

Acquisition notes Show
Lead V3 reversed on acquisition.

Ctrl+F for "reversed" opens the section and highlights it. The page answers the question the clinician actually asked.

Don't

A button, a region, and hidden

Ctrl+F reports no match, so the reader concludes the page does not mention it. The text is right there. This fails silently and never shows up in testing, because the tester knows where it is.

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/accordion.json
<Accordion>
  <AccordionItem
    title="Device & acquisition notes"
    summary="2 advisories, both acknowledged"   // required: a closed section must describe itself
    headingLevel={3}
  >
    <p>Lead V3 reversed on acquisition.</p>
  </AccordionItem>
</Accordion>

// No 'exclusive' prop and no 'name' on the underlying <details>, so one
// section cannot close another. The constraint is the absent API, not a
// runtime check.
// Panel used to ship with this install command and is not an accordion:
// a Panel is a titled container, an Accordion is a set of sections that
// open and close. It moved to the layout item — see Grid & panels.

What the component takes. This table used to list multiple, heading, LayoutPanel, subjectId — props the component does not have, and never had. Found by the registry gate’s props-table rule.

PropTypeDefaultNotes
childrenReact.ReactNode— Required. The AccordionItems.
typedivider | filleddivider Set on the accordion and read by every item: a node that looks unlike its neighbours reads as a different kind of thing.
titlestring— Required, on the item. The section’s heading.
summarySectionSummary Required. Count plus highest contained priority; used in the closed header and the accessible name.
secondarystring— On the item. A time, a source or a scope — never the finding itself.
metaReact.ReactNode— On the item. The count and highest contained priority, as a Badge and a Status chip.
defaultOpenbooleanfalse On the item. Closed unless there is a reason; the header must make a closed section safe to leave.
scrollContentbooleanfalse On the item. The content scrolls rather than growing the page, where every header must stay in view.
headingLevel2 | 3 | 4 | 5 | 63 On the item. The level the surrounding outline needs.
NotJustAnyMed.Tech Design System · Accordion · v1.0 · draft for review
Reference applications named in this system are fictional; all patient data and reference ranges shown are fabricated and illustrative.