A deep branch, opened
Every pct-tree-item can hold more items; expanded opens a branch from the start, and selected is a two-way value — set it, and the tree shows the way to that node.
Selected: apps/docs/src
A tree the reader walks with the keyboard: branches that open, leaves that get chosen.
@pacit/components/treepct-tree, pct-tree-itemImplements the W3C ARIA APG Tree View pattern ↗
Selected: src/app.ts
import { Component, signal } from '@angular/core';
import { PctTree, PctTreeItem } from '@pacit/components/tree';
/** One tabstop, arrows do the walking — a file picker in eight declarative lines. */
@Component({
selector: 'demo-tree',
imports: [PctTree, PctTreeItem],
styles: ':host { display: block; inline-size: min(100%, 20rem); }',
template: `
<pct-tree ariaLabel="Project files" [(selected)]="chosen">
<pct-tree-item value="README.md">README.md</pct-tree-item>
<pct-tree-item value="src" [expanded]="true">
src
<pct-tree-item value="src/app.ts">app.ts</pct-tree-item>
<pct-tree-item value="src/theme.ts">theme.ts</pct-tree-item>
</pct-tree-item>
</pct-tree>
<p>Selected: {{ chosen() ?? 'nothing yet' }}</p>
`,
})
export class TreeDemo {
readonly chosen = signal<string | null>('src/app.ts');
}tree.demo.ts — the same file runs here and is highlighted under Code
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
import { PctTree, PctTreeItem } from '@pacit/components/tree';| Exported | What it is |
|---|---|
PctTreecomponentpct-tree | A tree: a hierarchy the user walks — a file explorer, a nested outline. |
PctTreeItemcomponentpct-tree-item | One node: role="treeitem", its own text as the label, and any nested pct-tree-item elements pulled into the branch's role="group" — the consumer writes the tree as the tree, and the two-slot projection does the sorting. |
<pct-tree ariaLabel="Files" [(selected)]="chosen">
<pct-tree-item value="src" [expanded]="true">
src
<pct-tree-item value="src/app.ts">app.ts</pct-tree-item>
</pct-tree-item>
</pct-tree>Each example is the component itself, not a screenshot. Open the code under any of them.
Every pct-tree-item can hold more items; expanded opens a branch from the start, and selected is a two-way value — set it, and the tree shows the way to that node.
Selected: apps/docs/src
Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.
A tree: a hierarchy the user walks — a file explorer, a nested outline.
| Name | Type | What it does |
|---|---|---|
selectedtwo-waydefault null | string | null | The chosen item's value — the one thing the tree owns (the pagination's ownership at a hierarchy). A model, because both directions are ordinary: the application preselects, and Enter, Space or a click writes back. null is "nothing chosen". |
ariaLabeldefault '' | string | The accessible name of the tree. Optional, the chips' reasoning: a library default would have to guess what hierarchy this is, and two trees on one page are told apart here. |
(click), (focus), the form's submit — are the platform's, and two-way models above carry the rest. The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-tree | |
role | tree | |
aria-label | ariaLabel() || null |
Two ways in, both stable. No class names to depend on.
In a test, PctTreeHarness, PctTreeItemHarness from @pacit/components/testing read a part by the name below and a state by its attribute — the harness gate holds the names to the package.
| Part | Selector | What it is |
|---|---|---|
arrow | [data-pct-part="arrow"] | the toggle that expands a branch |
label | [data-pct-part="label"] | a node's text, the row that is selected |
Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.
| Token | Styles |
|---|---|
--pct-tree-arrow-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of the arrow. |
--pct-tree-arrow-size12px | Size of the arrow. |
--pct-tree-gap{pct.space.2}4px | Gap between nodes. |
--pct-tree-indent{pct.space.5}16px | Indent of the tree per level. |
--pct-tree-label-bg-hover{pct.surface-100}#f1f5f9 · #1e293b | Background of the label on hover. |
--pct-tree-label-bg-selected{pct.primary}#2563eb · #60a5fa | Background of the label when selected. |
--pct-tree-label-fg-selected{pct.on-primary}#ffffff · #0f172a | Text colour of the label when selected. |
--pct-tree-label-padding-x{pct.space.3}8px | Horizontal padding of the label. |
--pct-tree-label-padding-y{pct.space.2}4px | Vertical padding of the label. |
--pct-tree-label-radius{pct.radius.md}8px | Corner radius of the label. |
--pct-tree-label-target-min{pct.target.min}24px | Touch-target floor of the label. |
A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.
[data-theme='brand'] {
--pct-tree-label-bg-selected: #0f766e;
--pct-tree-label-fg-selected: #ffffff;
--pct-tree-arrow-fg: #0f766e;
}Selected: src/app.ts
Pattern: Tree View — (0056) one tab stop, a roving focus, treeitems in groups, aria-expanded on branches and aria-selected on the chosen row. Named in the class JSDoc.
| key | effect | test |
|---|---|---|
Tab / Shift+Tab | enters at the roving item / leaves the whole tree | apps/sandbox-e2e/src/tree.spec.ts — "one tab stop" |
ArrowDown / ArrowUp | next / previous visible row | tree.spec.ts — "the arrows walk visible rows" (and unit, tree.spec.ts in the lib) |
ArrowRight (LTR) | opens a closed branch; steps into an open one | the same case; the RTL swap: apps/sandbox-e2e/src/rtl.spec.ts |
ArrowLeft (LTR) | closes an open branch; otherwise climbs | the same two cases |
Home / End | first / last visible row | tree.spec.ts — both readings |
Enter / Space | chooses the row under focus | tree.spec.ts — "Enter chooses"; Space in the unit suite |
Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.
libs/components/tree/src/tree.tsapps/sandbox-e2e/src/a11y.spec.ts — /tree in SBX_ROUTESapps/sandbox-e2e/src/visual.spec.ts — tree-project and tree-project-rtlforced-colors: active — no state carried by colour alonelibs/components/tree/src/tree-item.scss — the chosen row in Highlight/HighlightText (the aria state made visible), the arrow in CanvasText. The reading: apps/sandbox-e2e/src/forced-colors.spec.tsprefers-reduced-motion — duration from a tokennot applicable — no motion at alllibs/components/tree/src/tree-item.scss — min-block-size: var(--pct-tree-label-target-min) on every row (lesson-139's floor, applied at design time this once)--pct-control-height-*deliberately not: rows are typography plus a target floor, not controls on the shared axisdir="rtl" screenshotapps/sandbox-e2e/src/rtl.spec.ts — the indent mirrors (a logical padding) and the WALK swaps its inline pair under real keys. The screenshot: tree-project-rtlNG05xxapps/sandbox-e2e/src/hydration.spec.ts — /tree in SBX_ROUTESselected is a UI model, not a form value; a form that wants it binds it itselflibs/components/parts.snapshot.md — label, arrowcontrast.policy.jsonlibs/tokens/src/contrast.policy.json — the chosen pair (AA), the chosen fill against the page (UI error), the hover lift and the arrow (UI warn); libs/tokens/tokens.snapshot.mdPCT_TEXTSnot applicable — the component draws no text of its ownlibs/components/size.snapshot.md — the ./tree rowdocs/acr/at/ — three readers, whole sandbox. The one tab stop is confirmed by the instrument itself: all three walks of this view end after a single Tab, because the roving tabindex leaves nothing else to land on. Orca announces README.md. · tree level 1 and VoiceOver README.md outline row (1 of 5) — name, role and, for VoiceOver, the position. The walk never presses an arrow, so the announcement on MOVING through the walk is not hereapps/sandbox/src/app/views/tree/ (the sandbox view). The published site: /components/tree — prerendered, the demo's own source is the code tablibs/components/tree/src/tree.spec.ts — 21 cases; libs/components/mutation.snapshot.md — tree.ts measured in the day's shared full runRead at build time from the tracked snapshots, not typed here.
lesson-21 said "before the second"; the extraction into core now has three consumers waiting, recorded against the plan's behaviour-layer item.aria-multiselectable is an announcement model of its own; a decision of its own the day a real application asks.aria-busy on a loading branch is 0037 at a hierarchy — its own future record.