v4 · Web Components · MIT · zero-dependency core

Tree views
for any framework.

A headless state engine plus two native custom elements — <sp-tree> and <sp-tree-select> — with cascade selection, lazy loading, search, and full keyboard navigation. Works in React, Vue, Svelte, Angular, and plain HTML.

<sp-tree selection="multi" cascade searchable> — live
Loading demo…

This is the real widget — search for mu, expand Canada (lazy), or navigate with Space.

Why sp-treeview

One tree, everywhere

v4 is a ground-up rewrite of the original Angular plugin: all view state lives in a store keyed by node id — never mutated onto your data.

🧩

Any framework

Native Web Components — drop <sp-tree> into React, Vue, Svelte, Angular, or plain HTML. No wrapper packages.

🧠

Headless core

A zero-dependency TypeScript state engine (TreeStore) owns selection, cascade, lazy loading, and filtering. Bring your own UI if you want.

Accessible

The WAI-ARIA tree pattern, done properly: roles, aria-checked (incl. mixed), roving tabindex, arrow keys, Home/End, type-ahead.

🎨

Fully themeable

Every visual value is a --sp-tree-* CSS custom property, with ::part() for structure. Light + dark defaults built in.

Live demos

Experience it — these are the real elements

Every widget below is the actual published component running in your browser, each with its own store and its own copy of the data.

Dropdown select field

form-associated

A form field whose value is a tree selection — removable chips, embedded search, and a real form value (JSON array of checked ids) via ElementInternals.

Open the field and pick a few regions.

Overlay select field

variant="overlay"

The same field as a full-viewport modal sheet — better on small screens. The panel is removed from the DOM while closed: no hidden tab targets.

Open the field — it takes over the viewport.

Single select (radio)

selection="single"

Exactly one selected node, persisted in the store and reported as detail.selected — the legacy “radio never reached the model” bug is structurally gone.

Pick any node.

Lazy loading & errors

loadChildren

Branches with hasChildren: true load on first expand — with a per-node spinner, and an inline error row with Retry when the loader rejects. Try Atlantis.

Expand a lazy branch.

Theming

Restyle it without touching the source

Every color, size, and font flows through a --sp-tree-* custom property; structure is exposed via ::part(). Drag the knobs — the CSS below is all it takes.

 

Bring your own framework

Standard custom elements — no wrappers

Quickstart

Up in under a minute

# install the Web Components (pulls in the core, lit, @floating-ui/dom)
npm install @sp-treeview/element

# markup
<sp-tree selection="multi" searchable></sp-tree>

# wire it up — object inputs are DOM properties
import '@sp-treeview/element';

const tree = document.querySelector('sp-tree');
tree.data = [
  { id: 'india', label: 'India', children: [{ id: 'mum', label: 'Mumbai' }] },
  { id: 'usa', label: 'USA', hasChildren: true },  // lazy branch
];
tree.loadChildren = async (node) => [{ id: 'ca', label: 'California' }];
tree.addEventListener('sp-change', (e) =>
  console.log(e.detail.checked.map((n) => n.label)));

Explore

Documentation