Tree
A list that admits it has depth — and pays for it, so it has to be the clinician's own mental model.
Overview
A tree earns its complexity only when the hierarchy is one the clinician already thinks in. Otherwise it costs two presses to reach every leaf and hides the size of the set behind a triangle. It is not a variant of a List: it carries its own expansion state, its own traversal, and one rule a flat list never needs — that a closed branch has to say what is inside it.
Tree
A tree is a list that admits it has depth. It earns its complexity only when the hierarchy is the clinician's own mental model; otherwise it costs two clicks to reach every leaf and hides the set size behind a triangle.
-
Encounter · 13 Aug 2026
-
ECG 2
- 15:41:02 · ST elevation V2–V4
- 14:26:38 · no acute change
- Laboratory 6 1 critical
- Imaging 0 none ordered
-
ECG 2
The collapsed Laboratory branch still reports that something inside it is critical. Collapse is allowed to cost you a click; it is not allowed to cost you a finding.
- Three levels maximum. Past that, the indentation stops being readable and the path stops being memorable. Flatten, or move to a breadcrumb plus a list.
- Branches report their contents — a count, and any state inside them at advisory priority or above.
- Expansion state is remembered per session, not reset on every navigation. Re-collapsing a branch the clinician opened is a small, repeated theft of attention.
- Never auto-collapse a branch the user opened when a sibling is expanded. Accordion behaviour in a tree loses work.
- A branch is never the only route to its leaves. Search reaches every leaf regardless of expansion state, and expands the path to what it finds.
What a closed branch owes the reader
Collapsing may hide detail. It may never hide state. A branch that is shut still declares what is inside it that would change a decision — the count, and the highest priority among them.
- A triangle is not a summary. The reader cannot tell an empty branch from one holding a critical result, and the only way to find out is to open every one — which is the work the tree was supposed to save.
- Expansion state is the reader's, and it is not inherited. A tree that opens where the last clinician left it presents someone else's reading of the record as the shape of it. On a shared workstation it is reset with the session.
- Depth is bounded. Past two levels inside a context, “where am I” stops being answerable from the screen — the same bound as Navigation.
- Closed is hidden from view, not from search. A branch behind a plain
hiddenis invisible to Ctrl + F, so the browser reports no match and a clinician searching the record for a drug name is told it is not there. The group useshidden="until-found"and opens itself onbeforematch, so the control’s state and the revealed content agree. This was plainhiddenuntil the interaction gate was pointed at it — the same defect Tabs had, on a page that already cited that rule. - Nothing that must not be missed lives in a closed branch. An active alarm renders in the docked region regardless of what is open, exactly as it does for Tabs. Concealment is what both components are for, which is why both need the same rule.
What the component enforces, and what it only asks for
One rule is held here and it is the one the component exists for; the rest is the shape of the data you pass.
| Rule | Held by | How |
|---|---|---|
| A closed branch declares what is inside it | The component | summary is on the type, so a branch labelled only “Studies” is not expressible.Asserted: Tree: a closed branch declares what is inside it |
| Closed is hidden from view, not from search | The component | hidden="until-found", opening on beforematch. It was plain hidden until an interaction test was written for it.Asserted: Tree: a closed branch is hidden from view, not from search |
| Expansion state is announced | The component | aria-expanded on each branch, with the whole label row as the target.Asserted: AX tree: the tree item reports expanded, and its group appears when it is |
| One tab stop, and arrows traverse what is open | The component | The items carry a roving tabindex; up, down, Home and End move between the items whose ancestors are all open, so a closed branch’s children are skipped. Driven in a browser. Asserted: Tree: one tab stop, and arrows traverse only what is visible |
| Right opens and enters; left closes and leaves | The component | Right opens a closed branch and steps into an open one; left closes an open branch and otherwise steps out to the parent; Enter and Space toggle. The tree held role="tree" for a long time with a button in every item and none of this behind it.Asserted: Tree: right opens then enters, left closes then leaves |
| The summary is true | You | “2 recordings, 1 critical” is a string. Nothing counts the children to check it, and a summary that under-reports is worse than none. |
| Depth stays within two levels | You | Nesting is whatever you render. |
| Expansion state does not survive a user switch | You | The component holds it in local state and knows nothing about sessions. |
A rule stated on a component page reads as a rule the component keeps. 3 of these 6 are; 3 are obligations this page was quietly handing to the reader. The summary is the whole safety argument for collapsing anything, and the component requires one without being able to check it.
Do's and don'ts
- Laboratory 6 1 critical
Collapsed, but the branch still declares what is inside it that would change a decision.
- Laboratory
A critical result one click away and no indication it is there. Collapse has hidden state, not depth.
▸ ECG 2 recordings, 1 critical
Closed, and still declaring what is inside. The reader knows there is something critical here without opening it.
▸ ECG
A closed branch with nothing said. A critical result is one press away and nothing on screen suggests it.
▾ Encounter · 13 Aug
▸ ECG 2 recordings
▸ Bloods 4 results
Two levels. The reader can say what they are looking at from the screen.
▾ Patient
▾ Encounters
▾ 13 Aug
▾ Investigations
▸ ECG
Five levels deep. “Where am I” is no longer answerable from the screen, and every leaf is five presses away.
Accessibility
- A real
treerole withtreeitemchildren, or nested lists witharia-expandedon the branches. A div with a rotating triangle is a picture of a tree. - Arrow keys traverse and expand — right opens, left closes or moves to the parent — which is what makes depth navigable without a pointer.
- A branch announces its state and its count, so a screen-reader user gets what the summary line gives a sighted one.
- Only one tab stop. A tree of forty nodes must not cost forty tab presses to pass.
Outcomes of use
- Effectiveness — the clinician can tell what is inside a branch without opening it, which is the only reason to close one.
- Efficiency — fewer presses than a flat list, which is the entire justification for the extra structure.
- Satisfaction — a shape that matches how the record is actually organised. Attributes believed to contribute to an outcome; the outcome is settled by observing real use, not by this page — see Usability & context of use.
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.
- A closed branch declares the count and the highest priority inside it. (the component declares it; the count is you) Mitigates: a critical result one press away with nothing on screen saying it is there.
- No alarm renders only inside a branch. (you) Mitigates: an alarm concealed by a collapsed node.
- Expansion state does not survive a user switch. (you) Mitigates: one clinician's reading of a record presented to the next as the record's own shape.
Implementation
$ npx shadcn@latest add https://md.notjustany.tech/r/tree.json
<Tree label="Encounter · 13 Aug 2026">
<TreeItem label="ECG" summary="2 recordings, 1 critical">
// the summary is required: a closed branch has to say what is in it
</TreeItem>
</Tree>