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.
This is the real widget — search for mu, expand Canada (lazy), or navigate with ↑ ↓ ← → Space.
Why sp-treeview
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.
Native Web Components — drop <sp-tree> into React, Vue, Svelte, Angular, or plain HTML. No wrapper packages.
A zero-dependency TypeScript state engine (TreeStore) owns selection, cascade, lazy loading, and filtering. Bring your own UI if you want.
The WAI-ARIA tree pattern, done properly: roles, aria-checked (incl. mixed), roving tabindex, arrow keys, Home/End, type-ahead.
Every visual value is a --sp-tree-* CSS custom property, with ::part() for structure. Light + dark defaults built in.
Live demos
Every widget below is the actual published component running in your browser, each with its own store and its own copy of the data.
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.
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.
Exactly one selected node, persisted in the store and reported as
detail.selected — the legacy “radio never reached the model” bug is
structurally gone.
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.
Theming
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
One import registers the elements. Set data as a property, listen for sp-change. No build step required.
⚛️Use the elements directly in JSX; bind object inputs through a ref. Typed, no wrapper package.
💚Mark sp-* as custom elements and bind through a template ref. Works with Vite out of the box.
Quickstart
# 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