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.
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
| Part | Rule |
|---|---|
| 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
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.
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.
- Never exclusive-open by default. Opening one section must not close another; comparing two sections is a normal thing to want.
- Never collapse what the clinician opened, on refresh, on data arrival, or when a sibling opens.
- Never nest accordions. Two levels of collapse is two chances to lose something, and the second level is almost never opened.
- Never put a form across sections. Validation errors in a collapsed section are errors the clinician cannot see; if a form is long, use Tabs with a visible progress statement or a single scrolling panel.
- Never animate longer than 150 ms, and honour
prefers-reduced-motion— see Motion. - Headers are real headings containing a button, so the section structure is navigable by heading, not just by tab.
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.
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.
Prior recordings
Acquisition notes
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.
| Construction | Use 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
- A fragment pointing inside a closed section opens it. Linking to
#acquisition-notesand landing on a collapsed node is the same failure as find-in-page, arriving by a different route. - Collapsing a section above the viewport must not move the content below it. The reader's place is theirs. Where the browser's scroll anchoring does not hold it, hold it explicitly — see Reprioritisation for what content moving under a reader costs.
- Everything expands for print. A collapse on paper is a deletion, per Printed output.
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.
| Part | Visible closed | Rule |
|---|---|---|
| Node | Yes | The whole header is the control. Minimum 44 px, per Touch & gestures. |
| Label | Yes | A word or short phrase naming what is inside. Required. |
| Secondary text | Yes | Optional. Qualifies the label — a time, a source, a scope. Never the finding itself. |
| Metadata | Yes | A count and the highest priority contained, as Badge and Status chip. This is the part that makes a closed node safe. |
| Caret | Yes | Decorative. aria-expanded carries the state; the caret only mirrors
it. |
| Content | No | A region labelled by the node, so its boundary is announced on entry and exit. |
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
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
| Size | Node height | Where |
|---|---|---|
| Default | 44 px | Everywhere. This is the floor from Touch & gestures, not a preference. |
| Compact | 32 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
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
- Caret side. Leading or trailing, chosen once and identical on every node. A caret that moves between nodes turns a scan into a search.
- Metadata slot order. Count first, then priority, then the caret. Fixed, because a reader locates these by position long before they read them.
- Counts are exact. No capping, per Badge — the difference between 9 and 90 prior recordings is the difference between glancing and planning.
- Priority is the highest contained, not a summary of the section's mood. If anything inside is advisory, the closed node says advisory.
- No required-field marker on a node. Other systems place an asterisk on a node whose content holds a required field. Two rules here forbid needing one: forms do not span sections at all, and "required" is stated in words rather than punctuation — see Text field.
Behaviour
| Behaviour | This 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
- Spacing. Divider type stacks with no gap. Filled type stacks with 4 px between nodes.
- Padding. 0.8 rem inside the node, and content padding matching it so the label and the text beneath it share a leading edge.
- Placement. Inside a panel, or directly on the page surface. Never inside another accordion — two levels of collapse is two chances to lose something, and the second level is almost never opened.
- Caret direction. Down when closed, up when open. This is the one thing the
caret is for, and it is still decorative:
aria-expandedis what is actually announced. - Right-to-left. The node mirrors — label, secondary text, metadata and caret all swap edge. Nothing inside the content mirrors that would not mirror on its own, so a trace or a time axis keeps its direction, per International design.
States
| State | Rendering |
|---|---|
| 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.
| Rule | Held by | How |
|---|---|---|
| A closed section describes itself | The 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 another | The 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 + F | The 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 announcement | The platform | Free, and it survives a bundle that failed to load. |
| Every item has the accordion’s type | The 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 compact | The 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 words | The 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 document | You | headingLevel is yours to set correctly; nothing knows what surrounds it. |
| Nothing that must not be missed is inside | You | The component hides what you give it. An unacknowledged finding in a collapsed section is concealed exactly as well as anything else. |
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
Closed, but the header declares what is inside. Collapse costs a click, not a finding.
A vague label hiding an advisory. Nothing on screen gives the clinician a reason to open it.
An active finding is rendered, not collapsed. Nothing that changes the next action lives behind a caret.
A critical alarm collapsed behind a count. The badge is red and still nobody opens it during a busy shift.
One panel, one subject, named in the header. The boundary is a claim the header can back up.
Three patients' trajectories inside one border, under a header that names none of them.
Two sections open at once. Comparing detail against priors is the reason both exist.
Exclusive-open. Opening the priors closed the analysis the clinician was reading, and they have to remember what it said.
One type throughout. The nodes look like members of one set, which is what they are.
Divider above, filled below. The difference looks deliberate, so a reader hunts for the meaning it carries. There isn't one.
Two words, then the qualifier on its own line. Scannable down a column of eight nodes, which is how it will actually be read.
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.
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.
The caret has moved edges and the chip has changed places. Scanning the column is now a search of each node in turn.
44 px on anything a finger may reach. The default, and the only size available unless the deployment has no touch input at all.
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.
<details> —
or hidden="until-found" if hand-built
Acquisition notes
Ctrl+F for "reversed" opens the section and highlights it. The page answers the question the clinician actually asked.
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
- Collapsed content stays findable. Native
<details>is opened by find-in-page; a hand-built region useshidden="until-found"and handlesbeforematch. A plainhiddenmakes the text unreachable by the browser's own search — see How to build one. - Header is a native
<summary>containing a heading (headingLevel, default 3), so the disclosure is announced as a disclosure with its state, and the title still sits in the page outline. Screen-reader users navigate by heading far more than by tab. This bullet used to describe a heading wrapping a button, which is the hand-built construction the component was rebuilt away from. - Expanded state is the element’s own.
<details>carries it; there is noaria-expandedoraria-controlsto keep in step, and nothing to drift. - The native marker is hidden and the state is in words. A glyph is not a state.
- Contained state is in the accessible name — "Device and acquisition notes, 1 advisory, collapsed" — not only in a coloured chip.
- Panels use
<section aria-labelledby>so the boundary the border draws visually exists non-visually too. - Panel borders meet 3:1 against both surfaces they separate (SC 1.4.11). This is verified in CI against tokens.
- Expansion animates ≤ 150 ms and respects
prefers-reduced-motion, which disables the height transition entirely. - No scroll trapping. An expanded section extends the page; it does not create an inner scroll area the user cannot escape by keyboard.
- Targets ≥ 24 px — the whole header row is the target, not just the caret (SC 2.5.8).
- Find-in-page. Where the platform supports it, collapsed content uses
hidden="until-found"so browser search still reaches it. Nothing safety-relevant depends on that support existing.
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 decision-relevant is behind a caret, and closed sections still declare what they hold.
- Efficiency — a screen dense enough to work in, without the cost of opening three sections to answer one question.
- Satisfaction — sections that stay as they were left. Re-collapsing what someone opened is a small betrayal, repeated all shift.
Clinical safety notes
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.
- Alarms and active findings are never collapsible. (you) Mitigates: a critical finding never read because it was behind a caret.
- Closed headers declare contained count and priority. (the component) Mitigates: no cue to open the section that matters.
- Non-exclusive expansion. (the component) Mitigates: losing the section being read when another is opened.
- Expansion state preserved across data arrival and refresh. (you) Mitigates: re-reading work already done, and missing that content changed.
- No forms split across collapsed sections. (you) Mitigates: validation errors hidden from the person being asked to fix them.
- One panel, one subject, named in the header. (the component names it; one subject is you) Mitigates: two patients' data read as one patient's.
- Panels degrade with a stated reason rather than disappearing. (you) Mitigates: a missing panel read as absent data rather than a failure.
- Sections never removed silently. (you) Mitigates: a permission or availability state indistinguishable from non-existence.
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.
| Prop | Type | Default | Notes |
|---|---|---|---|
children | React.ReactNode | — | Required. The AccordionItems. |
type | divider | filled | divider |
Set on the accordion and read by every item: a node that looks unlike its neighbours reads as a different kind of thing. |
title | string | — | Required, on the item. The section’s heading. |
summary | SectionSummary |
Required. Count plus highest contained priority; used in the closed header and the accessible name. | |
secondary | string | — | On the item. A time, a source or a scope — never the finding itself. |
meta | React.ReactNode | — | On the item. The count and highest contained priority, as a Badge and a Status chip. |
defaultOpen | boolean | false |
On the item. Closed unless there is a reason; the header must make a closed section safe to leave. |
scrollContent | boolean | false |
On the item. The content scrolls rather than growing the page, where every header must stay in view. |
headingLevel | 2 | 3 | 4 | 5 | 6 | 3 |
On the item. The level the surrounding outline needs. |
Related
- Affordance & perceivability — the general rule this page applies: a control exists only if every reader who can operate it can perceive it.
- Card — when the container is a discrete object rather than a region of a layout.
- Tabs — mutually exclusive views, where an accordion would be wrong.
- Tree — the same collapse rule applied to hierarchy.
- Toolbar — what may sit inside a panel's border, and why.
- Elevation — why panels stay flat.
- Motion — the 150 ms ceiling and reduced motion.