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).

Components API Guides Getting started Styling Espalier Browser support GitHub npm package Taproot I/O