Tree View
stimeo--tree-view
A single-select tree: navigate, expand/collapse, and typeahead with the keyboard.
The stimeo--tree-view controller implements the WAI-ARIA Tree View (single-select) pattern. Parent/child structure is read from the DOM nesting (a treeitem followed by its child group). The whole tree is one Tab stop (roving tabindex). ArrowDown/ArrowUp move between visible items; Home/End jump to the first/last visible item; printable characters typeahead by label prefix. ArrowRight expands a collapsed parent or steps into its first child; ArrowLeft collapses an expanded parent or steps to the parent item. Enter/Space/click select the item (single selection via aria-selected). aria-expanded and each child group's hidden stay in sync, dispatching stimeo--tree-view:toggle; selection dispatches stimeo--tree-view:select.
-
app
-
controllers
- application_controller.rb
- components_controller.rb
- application.rb
-
controllers
- README.md
- Gemfile
Keyboard
| Key | Action |
|---|---|
| ↓ / ↑ | Move to the next / previous visible item. |
| → | Expand a collapsed parent, else step into the first child. |
| ← | Collapse an expanded parent, else step to the parent item. |
| Home / End | Move to the first / last visible item. |
| Enter / Space | Select the item. |
| Printable characters | Typeahead to the next item whose label starts with the text. |
<%# Markup for the tree-view (tree view / single selection) demo.
Nested role="tree" / "treeitem" / "group" express the hierarchy. Arrows move between
visible items; ArrowRight / ArrowLeft expand/collapse and move between parent/child;
Home/End, typeahead, and Enter/Space select a single item. The library handles roving,
aria-expanded / aria-selected sync, and toggling each group's hidden. Across the whole
tree only one treeitem is tabindex=0. %>
<ul class="tree-view" data-controller="stimeo--tree-view" role="tree"
aria-label="<%= t('components.tree_view.demo.label') %>">
<li
class="tree-view__item"
role="treeitem"
aria-expanded="true"
aria-selected="false"
tabindex="0"
data-stimeo--tree-view-target="item"
data-action="keydown->stimeo--tree-view#onKeydown click->stimeo--tree-view#onClick">
<span class="tree-view__label">app</span>
<ul class="tree-view__group" role="group" data-stimeo--tree-view-target="group">
<li
class="tree-view__item"
role="treeitem"
aria-expanded="false"
aria-selected="false"
tabindex="-1"
data-stimeo--tree-view-target="item"
data-action="keydown->stimeo--tree-view#onKeydown click->stimeo--tree-view#onClick">
<span class="tree-view__label">controllers</span>
<ul class="tree-view__group" role="group" data-stimeo--tree-view-target="group" hidden>
<li class="tree-view__item" role="treeitem" aria-selected="false" tabindex="-1"
data-stimeo--tree-view-target="item"
data-action="keydown->stimeo--tree-view#onKeydown click->stimeo--tree-view#onClick">
<span class="tree-view__label">application_controller.rb</span>
</li>
<li class="tree-view__item" role="treeitem" aria-selected="false" tabindex="-1"
data-stimeo--tree-view-target="item"
data-action="keydown->stimeo--tree-view#onKeydown click->stimeo--tree-view#onClick">
<span class="tree-view__label">components_controller.rb</span>
</li>
</ul>
</li>
<li class="tree-view__item" role="treeitem" aria-selected="false" tabindex="-1"
data-stimeo--tree-view-target="item"
data-action="keydown->stimeo--tree-view#onKeydown click->stimeo--tree-view#onClick">
<span class="tree-view__label">application.rb</span>
</li>
</ul>
</li>
<li class="tree-view__item" role="treeitem" aria-selected="false" tabindex="-1"
data-stimeo--tree-view-target="item"
data-action="keydown->stimeo--tree-view#onKeydown click->stimeo--tree-view#onClick">
<span class="tree-view__label">README.md</span>
</li>
<li class="tree-view__item" role="treeitem" aria-selected="false" tabindex="-1"
data-stimeo--tree-view-target="item"
data-action="keydown->stimeo--tree-view#onKeydown click->stimeo--tree-view#onClick">
<span class="tree-view__label">Gemfile</span>
</li>
</ul>
/*
* Presentation-only styles for the tree-view demo.
* The library drives expand/collapse via items' aria-expanded and groups' hidden,
* selection via aria-selected, and roving via tabindex. Here we add the hierarchy
* indentation and the selection / expansion look (the triangle marker, highlight).
*/
.tree-view {
margin: 0;
padding: 0;
list-style: none;
min-width: 18rem;
font-size: 0.9rem;
color: var(--fg, var(--color-text));
}
.tree-view__group {
margin: 0;
padding-left: 1.1rem;
list-style: none;
}
.tree-view__group[hidden] {
display: none;
}
.tree-view__label {
display: block;
padding: 0.25rem 0.4rem;
border-radius: 0.25rem;
cursor: pointer;
}
/* Add an expand/collapse marker to items that have children (not to leaves). */
.tree-view__item[aria-expanded] > .tree-view__label::before {
content: "▸";
display: inline-block;
width: 1rem;
color: var(--color-text-muted);
}
.tree-view__item[aria-expanded="true"] > .tree-view__label::before {
content: "▾";
}
.tree-view__item[aria-selected="true"] > .tree-view__label {
background: var(--vital-100);
font-weight: 600;
}
.tree-view__item:focus {
outline: none;
}
.tree-view__item:focus-visible > .tree-view__label {
outline: 2px solid var(--accent, var(--color-primary));
outline-offset: -2px;
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--tree-view"
Targets
| Name | Description | Attribute |
|---|---|---|
item
required
|
A treeitem; the whole tree is one Tab stop with arrow navigation, expand/collapse, and selection. | data-stimeo--tree-view-target="item" |
group
|
A role=group holding a treeitem's children; its hidden state syncs with the parent's aria-expanded. |
data-stimeo--tree-view-target="group" |
Actions
| Name | Description | Action |
|---|---|---|
onClick
|
Focuses and selects the clicked item (only the nearest treeitem to the target acts). | stimeo--tree-view#onClick |
onKeydown
|
Routes tree keys: arrows navigate/expand/collapse, Home/End, Enter/Space select, printable chars typeahead. | stimeo--tree-view#onKeydown |
Events
| Name | Description | Event |
|---|---|---|
select
|
Dispatched when an item is selected; detail carries { item }. |
stimeo--tree-view:select |
toggle
|
Dispatched when a parent is expanded or collapsed; detail carries { item, expanded }. |
stimeo--tree-view:toggle |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-expanded |
Parent item | Open/closed state of the child group. |
aria-selected |
Item | "true" on the single selected item. |
tabindex |
Item | Roving: active=0, others=-1 (the tree is one Tab stop). |
hidden |
Group | Present when the parent is collapsed. |