Tree
import "@taprootio/espalier/tree";
<esp-tree>
A form-associated hierarchy primitive for displaying and selecting
from a slug-structured tree — a folder/path picker, a category
chooser, or any place a flat field should become an expandable tree.
For static declarative node definitions, see
<esp-tree-item>.
The name is apt: an espalier is a tree trained flat against a frame, so the visual language — branches trained to vertical guide wires, grafting a new shoot onto an existing limb — is the component's own metaphor.
Data-driven first, composition-capable
Bind a hierarchy to the nodes property for the primary, data-driven
API. For static trees, nest
<esp-tree-item> children instead — they are
parsed into the same model, so the keyboard and accessibility model is
identical either way. When both are supplied, nodes wins. Data-driven
nodes must provide stable, unique id values; the tree keys expansion,
lazy loading, focus, and host re-feeds by id so relabeling or moving a
node can change its path without changing its identity.
Nodes may also carry opaque host data. The tree never reads into data;
it carries the value through select, edit, toggle, and graft events so a
host can round-trip its own model without a side lookup.
Selection and form value
Single-select for v1. Clicking a selectable row selects it; selecting
a branch opens it, and clicking an already-selected branch toggles it
open or closed. Selecting a row outside the open selected branch
collapses that branch unless keep-expanded is set. Branches can also
expand and collapse through the leading icon button or with keyboard:
ArrowRight expands closed branches first, then focuses a row's
trailing action button when one is present; ArrowLeft collapses or
moves to the parent. Enter and Space select the focused row. The
submitted form value is the selected node's full path
(defaulting to root-inclusive web paths like /weddings/2026), or
null when nothing is selected. The root row is always present,
defaults to label and path /, and is selectable by default; set
root-selectable="false" when the root should be navigation-only.
Validity reflects the committed selection through the standard
Espalier form flow.
Lazy loading
Mark a branch with hasChildren and set the loadChildren callback to
fetch its children on first expand. Expansion state is stable across
re-renders and reloads.
Grafting a new branch
Set allow-graft to expose inline + affordances at the level where
a new child would appear. Clicking one opens the built-in graft
dialog, which shows the parent and resulting path while the value is
normalized to a valid web-path segment. Enter commits, Escape
cancels. Keyboard users can press Insert on the focused row to add
beneath it. Newly grafted branches are ephemeral client state the
host reads back via events — the component persists nothing.
As the input changes, the tree emits a live esp-tree-graft-input
event. A consumer can run an async check the offline component cannot
(most importantly a server-side duplicate not present in the loaded
children) and push the result back with setGraftError(message); the
tree then shows it inline and blocks the commit until it is cleared.
For advanced add flows, listen for the cancelable
esp-tree-graft-request event and call preventDefault() to skip
the default dialog.
Replace the buildPath property to use non-web path semantics, and
replace normalizeGraftSegment to loosen, tighten, or completely
skip the default web-slug normalization used by the graft dialog.
Node actions
Mark a node with editable: true to render an edit action on the
trailing side of the row. Clicking it, or pressing F2 on the
focused row, emits esp-tree-edit with the node path and public node
data so the host can open its own editor. Optional flags
(deletable, movableUp, movableDown, cuttable, and
pasteChildTarget) expose the matching built-in editing actions for
host-owned tree workflows.
If a row has exactly one action, the tree renders one trailing
icon-only esp-button with an accessible label such as Edit About.
If a row has more than one action, the actions collapse into one
trailing esp-action-menu trigger so rows never accumulate multiple
visible action buttons. The tree only emits events — it does not
delete, move, cut, paste, mutate source nodes, or own clipboard state.
Expand/collapse icon
The expand/collapse caret ships inline with the component, so unlike
node icons it needs no icon sprite to be hosted. Override it by
setting the expandIcon property to your own (trusted) SVG markup; it
renders on every branch toggle and is rotated 90° when the branch is
open, so provide a right-pointing icon. For icon pairs that should not
rotate, set collapseIcon to the trusted SVG markup to use while the
branch is open.
Slots
<esp-tree> has a slot:
Default
Optional static esp-tree-item children describing the tree.
Attributes
<esp-tree> has the following attributes:
nodes
nodes has a default value of [].
The hierarchy to display, as the primary data-driven API. When
non-empty this takes precedence over slotted esp-tree-item
children.
value
value has a default value of "".
The selected node's full path (segments joined by /), or the
empty string when nothing is selected. This is also the form
submission value.
name
name has a default value of "".
The name used when the tree participates in a <form>.
required
required has a default value of false.
When true, a selection must be made before the form can submit.
required-message
required-message has a default value of "".
A custom message shown when the tree is required but nothing is
selected. Defaults to "Please select an option.".
disabled
disabled has a default value of false.
When true, the tree is disabled and cannot be interacted with.
allow-graft
allow-graft has a default value of false.
When true, an inline + affordance is shown on every node and at
the root for grafting a new child branch.
root-label
root-label has a default value of "/".
Label shown for the always-rendered root ancestor row.
root-path
root-path has a default value of "/".
Path submitted when the root row is selected. Defaults to /.
root-selectable
root-selectable has a default value of true.
Whether the root row can be selected. Defaults to true. Set
root-selectable="false" to make it navigation-only.
label
label has a default value of "".
An accessible name for the tree. Usually unnecessary inside an
esp-form-item, which provides the label; set it for standalone
use.
graft-error
graft-error has a default value of null.
A consumer-supplied error for the in-progress graft, shown inline
and blocking commit until cleared. Set it with setGraftError,
typically in response to an esp-tree-graft-input event after an
async check. Mirrored as the graft-error attribute.
keep-expanded
keep-expanded has a default value of false.
Keep already-expanded branches open when selecting a row outside of the selected branch. By default, selecting outside an open selected branch collapses that branch.
intent
intent has a default value of undefined.
The element's intent — its meaning: neutral
(no pin), or success, warning, danger, info — each pinned
to its fixed status family (blue for info), retunable per theme
via intents. On token-emitting controls a non-neutral intent pins the
filled-action pair to that family, derived over the governing
zone's theme; class-styled chrome (badges, callouts, status pills)
opts out of inline emission and renders its treatment from CSS
classes instead. An intent never repaints the surrounding zone,
which is what contexts are for. Removing the attribute restores
the element's own default intent.
context
context has a default value of undefined.
A theme-defined color zone. The selected context rebinds designer-facing roles and emits a complete, contrast-enforced semantic token table on this host for descendants to inherit.
Methods
<esp-tree> has the following methods:
validate
Re-run constraint validation and dispatch esp-validity-changed.
checkValidity
Check whether the current state is valid.
focus
Move focus to the active tree row.
setGraftError
Push a consumer-supplied error for the in-progress graft (or clear
it with null). While set, the error is shown inline and the graft
commit is blocked. Call this in response to an esp-tree-graft-input
event after an async server check.
traverseToClosest
Traverse up the DOM tree to find the closest element that matches the selector. This method is aware of shadow DOM boundaries and will traverse through them to find the element.
Events
<esp-tree> emits the following events:
esp-tree-select
esp-tree-select is of type CustomEvent<EspTreeSelectDetail>.
Fired when a node is selected.
esp-tree-toggle
esp-tree-toggle is of type CustomEvent<EspTreeToggleDetail>.
Fired when a branch is expanded or collapsed.
esp-tree-graft-request
esp-tree-graft-request is of type CustomEvent<EspTreeGraftRequestDetail>.
Cancelable event fired before the default graft dialog opens.
esp-tree-graft
esp-tree-graft is of type CustomEvent<EspTreeGraftDetail>.
Fired when a new branch is committed.
esp-tree-edit
esp-tree-edit is of type CustomEvent<EspTreeEditDetail>.
Fired when an editable node's edit affordance is invoked.
esp-tree-delete
esp-tree-delete is of type CustomEvent<EspTreeNodeActionDetail>.
Fired when a deletable node's delete action is invoked.
esp-tree-move-up
esp-tree-move-up is of type CustomEvent<EspTreeNodeActionDetail>.
Fired when a movable-up node's move-up action is invoked.
esp-tree-move-down
esp-tree-move-down is of type CustomEvent<EspTreeNodeActionDetail>.
Fired when a movable-down node's move-down action is invoked.
esp-tree-cut
esp-tree-cut is of type CustomEvent<EspTreeNodeActionDetail>.
Fired when a cuttable node's cut action is invoked.
esp-tree-paste-child
esp-tree-paste-child is of type CustomEvent<EspTreeNodeActionDetail>.
Fired when a paste child target node's paste-as-child action is invoked.
esp-tree-graft-input
esp-tree-graft-input is of type CustomEvent<EspTreeGraftInputDetail>.
Fired live as the graft input changes.
esp-validity-changed
esp-validity-changed is of type CustomEvent<{ valid: boolean; message: string }>.
Fired whenever constraint validation runs.
CSS Properties
<esp-tree> has the following CSS properties:
--esp-tree-indent-step
Horizontal indent added per nesting level. Defaults to 1.25rem.
--esp-tree-guide-color
Color of the branch guide path. Defaults to var(--esp-color-border).
--esp-tree-node-padding
Vertical padding of each node row. Defaults to var(--esp-size-small).
--esp-tree-action-size
Hit target size for tree icon actions. Defaults to 1.75rem.
--esp-tree-row-bg
Background color of an unselected row. Defaults to var(--esp-color-layer-1).
--esp-tree-row-border-color
Border color of an unselected row. Defaults to var(--esp-color-border).
--esp-tree-action-bg
Background color of the expand/collapse action rail. Defaults to a derived layer color.
--esp-tree-action-hover-bg
Hover background color of the expand/collapse action rail. Defaults to var(--esp-color-layer-2).
--esp-tree-hover-bg
Background color of a node row on hover. Defaults to var(--esp-color-layer-2).
--esp-tree-selected-bg
Background color of the selected node row. Defaults to var(--esp-color-layer-3).
--esp-tree-icon-color
Color of the expand/collapse action and node icons. Defaults to var(--esp-color-headings).
--esp-field-background
Background color of the shared field shell. Defaults to var(--esp-color-layer-2).
--esp-field-border-color
Border color of the shared field shell. Defaults to var(--esp-color-border).
--esp-field-border-width
Border width of the shared field shell. Defaults to 1px.
--esp-field-text-color
Text color used inside the shared field shell. Defaults to var(--esp-color-text).
--esp-field-hover-bg
Hover background color of the shared field shell. Derived from --esp-field-background.
--esp-field-focus-bg
Focus background color of the shared field shell. Derived from --esp-field-background.
--esp-field-focus-shadow
Shadow color used for shared field focus treatment. Defaults to var(--esp-color-shadow).