tree()

Progressively enhances an author-written, nested <ul>/<li> into an accessible tree view following the WAI-ARIA APG Tree View pattern — roving tabindex, arrow-key navigation, expand/collapse, single-select, and optional lazy-loaded children. It is a plain decorator like tooltip(): you own the markup and styling, tree() adds the roles and behaviour. Reach for it for a content/structure sidebar (chapters → sections, file trees, an outline). Source: tree.mjs.

You write plain nested lists; each <li>'s non-<ul> content is auto-wrapped in a focusable .ui-tree-row[role="treeitem"] and nested lists become role="group". A node is a parent (expandable) when its <li> contains a child <ul> or carries [data-lazy]; otherwise it is a selectable leaf. Use data-id to identify nodes in the on_select callback and the select() / reveal() handle. Mark an <li> [data-disabled] and its row is shown but not selectable — see §4.

The demos below put their source in a <template> and render it live with <ui-code for="…-tpl" into="…">: because tree() rewrites the live DOM (adding roles and row wrappers), a <template> keeps the shown markup pristine — see ui-code §Picking a target.

Accessibility (WCAG)

ExportSignatureDescription
tree tree(ul, { load?, on_select?, label?, should_typeahead?, loading_label?, error_label? }) Enhances an in-DOM <ul>. Returns a handle.

load(li) → children for a [data-lazy] node (HTML string, a <ul> to unwrap, or <li> nodes; may be async) · on_select(id, row) fires on user activation (not for programmatic select()) · should_typeahead defaults to true · loading_label / error_label override the two strings the lazy path shows and announces, which otherwise come localized ("Loading…", "Could not load the content") · Handle: select(id), reveal(id), refresh(), destroy() · Mark decorative content (icons, badges) aria-hidden="true" to exclude it from the row's accessible name.

Per-<li> attributes you write: data-id (node identity) · data-lazy (children fetched on first expand) · data-collapsed (start closed) · data-selectable (a parent that selects as well as toggles) · data-disabled[="reason"] (shown, announced unavailable, not selectable) · [data-tree-toggle] on an element inside a row (the chevron that toggles a data-selectable parent).

1. Basic tree

A nested list of chapters and sections. Click a chapter to expand/collapse it, click a section to select it; navigate with the arrow keys.

Show code

2. Lazy-loaded children

Mark a parent [data-lazy] with no child list; load() is called the first time it is expanded. While the promise is pending a loading row is shown and announced.

Show code

3. Programmatic selection

The handle's select(id) highlights a node and expands its ancestors — useful for keeping the tree in sync with external navigation (without firing on_select).

Show code

4. Disabled rows

Chapters 2 and 3 carry data-disabled="You do not have access". They are dimmed and the arrows still stop on them, so keyboard and screen-reader users can find them and read the explanation in the tooltip. Clicking one, or pressing Enter on it, logs nothing. Chapter 1 remains selectable; use it to confirm that the log works.

Chapter 3 is a disabled parent, and it still expands — by click, by →, or by Enter. This keeps its selectable child accessible.

Show code

See also