Lazy tree
A lazy tree defers deep branches: the
tree component renders with
an empty group, and the children arrive as server-rendered
<li class="hc-tree__item"> fragments the first time the user expands
the branch. installTree() dispatches a bubbling hc:treeexpand
when a branch opens; htmx listens with a once trigger and owns the
request — the behavior never touches the network.
Live demo
Section titled “Live demo”Expand a branch (click the row or press →) — the children
arrive from a demo endpoint under api/recipes/lazy-tree/ as
server-rendered treeitems, and once means re-collapsing and
re-expanding never refetches. Restricted shows the contract’s error
mitigation: an empty 200 clears the group’s aria-busy and an
HX-Trigger header raises the error toast — note the once trigger is
spent, so re-expanding it will not retry.
- Reports
- Assets
- Restricted
- readme.md
The lazy branch
Section titled “The lazy branch”<li class="hc-tree__item" aria-expanded="false" data-hx-get="/nodes/42/children" data-hx-target="find .hc-tree__group" data-hx-swap="innerHTML" data-hx-trigger="hc:treeexpand once"> <span class="hc-tree__row"> <span class="hc-tree__toggle" aria-hidden="true"></span> <span class="hc-tree__label">Reports</span> </span> <ul class="hc-tree__group"></ul></li>hc:treeexpand once— htmx fetches on the first expand only.onceis the whole trick: re-collapse/re-expand shows the already-loaded children with no further requests. Without it every expand refetches and re-swaps the subtree.find .hc-tree__group—findresolves to the first match inside the item: its own group.innerHTML— fill the group, keep the<ul>the behavior observes. While the request is in flight the behavior marks the empty grouparia-busy="true"(a CSS spinner renders; cleared when children arrive).aria-expandedmust be present — branch-ness is declared by the attribute, and the behavior only dispatcheshc:treeexpandon branches.
Server response
Section titled “Server response”Only the group’s innerHTML — treeitem fragments:
<li class="hc-tree__item"> <span class="hc-tree__row"> <span class="hc-tree__label"><a href="/reports/summary">summary.pdf</a></span> </span></li>Nested lazy branches inside the response recurse: give a fragment
the same four attributes and an empty group. htmx processes the new
data-hx-* attributes on settle, and the behavior re-applies roles
and the roving tabindex — swapped-in items are immediately
keyboard-navigable (→ descends into them as soon as they arrive).
Failure handling
Section titled “Failure handling”htmx does not swap non-2xx responses, so on an error the branch stays
empty — and the once trigger is spent: re-expanding will not
retry. Surface failures with an
HX-Trigger toast and prefer
server-side reliability; if a subtree fails often, render it eagerly
instead. The recipe documents this limitation honestly rather than
hiding it behind retry choreography.
Progressive enhancement
Section titled “Progressive enhancement”Without JavaScript the markup is a plain nested list — every rendered link keeps working. Either render the tree fully expanded on the server (lazy loading is purely an enhancement), or give each branch a real link to a per-node page: the links are the fallback.
Related
Section titled “Related”- Tree — the component: markup, keyboard model, states, tokens.
- Lazy panel — the same defer-until-relevant idea for flat regions.
- Toast — error surfacing for spent triggers.