Tree
Summary
An expandable hierarchy for resources, files and component
inventories. The markup is a nested list with
role="tree" semantics; branch items expand and
collapse on their chevron toggle, leaves take a spacer so labels
stay aligned. Auto-init binds every .soma-tree on
load. Expanded state lives on aria-expanded, and
the toggle's accessible name ("Collapse" / "Expand") is kept in
sync, localised. Keyboard follows the WAI-ARIA tree pattern:
the whole tree is one tab stop and the arrow keys walk it (see
Keyboard navigation).
When to use
| Pattern | Use it for |
|---|---|
| Tree | Hierarchies the operator browses and folds: environments > services, folders > files, component inventories. |
| List group | Flat row lists with no nesting. See List group. |
| Sidebar nav groups | One level of collapsible navigation in the shell sidebar; see the Shell page. |
| Accordion | Folding content sections, not items — see Expander. |
Examples
Keyboard navigation
The tree implements the WAI-ARIA tree pattern: the whole tree
is one tab stop. Treeitems carry a roving
tabindex (the current item is tabbable, the rest
are not), the arrows move focus through the visible
items, and the toggle buttons are taken out of the tab order
at bind. They stay as mouse affordances while keyboard and
screen-reader users act on the treeitem itself.
| Key | Action |
|---|---|
| Tab | Enter or leave the tree (one stop — focus lands on the current item). |
| ↓ / ↑ | Next / previous visible item. No wrap at the ends. |
| → | Expand a collapsed branch; on an expanded branch, step to its first child. (Flips with RTL — the keys follow visual direction.) |
| ← | Collapse an expanded branch; on a leaf or collapsed item, step to the parent. (Flips with RTL.) |
| Home / End | First / last visible item. |
| Enter / Space | Toggle an expandable item. |
Each toggle's accessible name is synced to its state
("Collapse" while expanded, "Expand" while collapsed) from the
tree.collapse / tree.expand i18n keys
(see Localisation below). The chevron rotation is
skipped under prefers-reduced-motion, and mirrored
under RTL: collapsed points left, expanded still points down.
The focused item paints the focus ring on its row.
HTML
The core contract. A branch has a toggle button and a nested
ul[role="group"], with aria-expanded on
the item; a leaf swaps the toggle for a spacer so labels
align:
<ul class="soma-tree" role="tree">
<!-- branch: has a toggle and a nested group; aria-expanded on the item -->
<li class="soma-tree-item" role="treeitem" aria-expanded="true">
<div class="soma-tree-row">
<button class="soma-tree-toggle"><span class="soma-icon soma-icon-chevron-right"></span></button>
<span class="soma-icon soma-icon-folder"></span>
<span class="soma-tree-label">prod-web</span>
</div>
<ul role="group">
<!-- leaf: a spacer instead of a toggle keeps labels aligned -->
<li class="soma-tree-item" role="treeitem">
<div class="soma-tree-row">
<span class="soma-tree-toggle-spacer"></span>
<span class="soma-icon soma-icon-file"></span>
<span class="soma-tree-label">api-backend</span>
</div>
</li>
</ul>
</li>
</ul>
Start a branch collapsed by authoring
aria-expanded="false"; on bind, any branch (an item
with a child group) that has no explicit attribute is normalised
to aria-expanded="true":
<li class="soma-tree-item" role="treeitem" aria-expanded="false">
<div class="soma-tree-row">…</div>
<ul role="group">…</ul>
</li>
Labels can be links. An a.soma-tree-label gets
hover styling and keeps the row layout; trailing content (badges)
sits after the label:
<div class="soma-tree-row">
<span class="soma-tree-toggle-spacer"></span>
<span class="soma-icon soma-icon-file"></span>
<a class="soma-tree-label" href="/services/api-backend">api-backend</a>
<span class="soma-badge soma-badge-success">running</span>
</div>
Branches nest by repeating the same item structure inside
ul[role="group"] — any depth. The chevron rotates
via CSS and mirrors in RTL.
CSS classes
| Class | Effect |
|---|---|
.soma-tree | Root <ul> with role="tree"; auto-init hook. |
.soma-tree-item | An item (role="treeitem"); branches carry aria-expanded, which hides the child group and rotates the chevron when false. |
.soma-tree-row | The visible row: toggle (or spacer) + icon + label, plus anything trailing (badges). |
.soma-tree-toggle | The chevron <button> on branch items; its aria-label is set and kept in sync by the component (localised). |
.soma-tree-toggle-spacer | Leaf-row placeholder so labels align with branch labels. |
.soma-tree-label | The item's text: a span, or a link (a.soma-tree-label) with hover styling. |
ul[role="group"] | The nested child list inside a branch item; hidden while the item has aria-expanded="false". |
JavaScript
Constructor
| Member | Description |
|---|---|
Soma.tree(elOrSelector) | Get or create the singleton for a .soma-tree root (throws when nothing matches or the class is missing). Binding normalises branch state (missing aria-expanded → "true") and sets every toggle's localised label. Auto-init covers markup present at load, so call this to reach the API — or to bind a tree rendered later. |
Instance methods
| Member | Description |
|---|---|
.expand(item) | Expand a branch: pass the .soma-tree-item element. Sets aria-expanded="true", re-labels the toggle, dispatches toggle. Items without aria-expanded (leaves) are ignored, and a redundant call (already expanded) no-ops without an event. |
.collapse(item) | Collapse a branch: same contract, incl. the redundant-call no-op. |
.toggleItem(item) | Flip the branch's current state. |
.on(event, fn) / .off(event, fn) | Subscribe / unsubscribe: 'toggle'. Listeners attach to the root element. |
.destroy() | Unbind and restore pre-init markup: aria-expanded the component added is removed (author-supplied values keep their current state), toggle aria-labels return to whatever the author wrote (or none), and the roving tabindex is stripped from items and toggles. |
All methods return the instance, so calls chain. The underlying
DOM event is a bubbling soma-tree-toggle
CustomEvent dispatched on the root with
e.detail = { item, expanded }, usable directly
with addEventListener for delegated listening
across every tree on the page. It fires for chevron clicks and
for each imperative expand/collapse/toggleItem
call.
Expand or collapse everything by iterating the branch items
(only items with aria-expanded are branches):
const tree = Soma.tree('#resources');
const branches = document.querySelectorAll('#resources .soma-tree-item[aria-expanded]');
branches.forEach((item) => tree.expand(item)); // expand all
branches.forEach((item) => tree.collapse(item)); // collapse all
Load children lazily — populate a branch's group on its first expansion:
Soma.tree('#resources').on('toggle', async (e) => {
const { item, expanded } = e.detail;
const group = item.querySelector(':scope > ul');
if (expanded && group.dataset.loaded !== 'true') {
group.dataset.loaded = 'true';
group.innerHTML = await fetchChildrenMarkup(item);
}
});
Bind a tree rendered after page load (auto-init runs once, at
DOMContentLoaded). The call is a get-or-create
singleton, safe after any DOM update:
container.insertAdjacentHTML('beforeend', renderedTreeMarkup);
Soma.tree(container.querySelector('.soma-tree'));
The live examples above, exactly as this page wires them:
const tree = Soma.tree('#tree-deep');
const branches = () =>
document.querySelectorAll('#tree-deep .soma-tree-item[aria-expanded]');
document.getElementById('tree-expand-all').addEventListener('click', () => {
branches().forEach((item) => tree.expand(item));
});
document.getElementById('tree-collapse-all').addEventListener('click', () => {
branches().forEach((item) => tree.collapse(item));
});
// Log every toggle from every tree on the page (the event bubbles).
document.addEventListener('soma-tree-toggle', (e) => {
const label = e.detail.item.querySelector('.soma-tree-label').textContent;
document.getElementById('tree-log').textContent =
`soma-tree-toggle — ${label}: ${e.detail.expanded ? 'expanded' : 'collapsed'}`;
});
Localisation
The toggle labels come from the tree.expand /
tree.collapse i18n keys ("Expand" / "Collapse" in
English). Override them via Soma.i18n
before components initialise — labels are written at
bind time and on every toggle (see
Themes and
i18n):
Soma.i18n({ locale: 'cs' }); // built-in pack
Soma.i18n({ 'tree.expand': 'Rozbalit', // or per-key overrides
'tree.collapse': 'Sbalit' });