Tree View
TreeView — a hierarchical tree with full keyboard support (↑/↓ move, →/← expand-or-descend / collapse-or-ascend, Home/End, Enter selects, Space toggles).
Real use · site page tree — drag a row to reorder or reparent it
- Pages
- Home
- About
- Pricing
- Shop
- Products
- Collections
- Checkout (locked)
- Blog
- Settings
Using Tree View
Silica ships one design system through three paths. They share a class vocabulary but are not interchangeable — pick the one that matches what you are building.
Plain HTML — CSS classes
The Tailwind v4 plugin. Works in any framework or none — Rails, Django, PHP, Go, a static file. This path ships no JavaScript, so Tree View is styled but inert: its behavior comes from tree. For that behavior with no framework, use the node-tree path below.
/* in your CSS */
@import "tailwindcss";
@plugin "@wizeworks/silicaui";Root class tree, with 8 modifiers:
.tree.tree-group.tree-item.tree-node.tree-node-icon.tree-node-label.tree-rename.tree-toggle.tree-toggle-spacer
React
Typed components over those same classes, with behavior and accessibility from Base UI.
npm i @wizeworks/silicaui-reactimport { TreeView } from "@wizeworks/silicaui-react";Props
| Prop | Type | Required | Description |
|---|---|---|---|
| items | TreeNode[] | yes | The node forest. |
| expanded | string[] | no | Controlled set of expanded node ids. |
| defaultExpanded | string[] | no | Uncontrolled initial expanded ids. |
| onExpandedChange | (expanded: string[]) => void | no | — |
| selected | string | no | Controlled selected node id. |
| defaultSelected | string | no | Uncontrolled initial selected id. |
| onSelectedChange | (id: string) => void | no | — |
| onSelect | (node: TreeNode) => void | no | Fires with the full node when one is selected. |
| onMove | (id: string, targetId: string, edge: TreeDropEdge) => void | no | Enables row drag-to-reorder/-reparent (rows become draggable) and fires once a drag is released over a valid row: edge is "before"/"after" a sibling or "inside" (append as a child). |
| onRename | (id: string, value: string) => void | no | Enables inline rename on rows marked renamable (double-click the row, or F2 on a focused one) and fires ONCE on commit — Enter or blur, never per keystroke, so a consumer with an undo stack gets one entry per rename rather than one per character. |
Source of the demo above
The exact file that renders the examples above, so it can never drift from them. It imports a few of this site’s own layout helpers (../lib/…), so read it for the component calls rather than copying it whole.
Show source
import { useState } from "react";
import { TreeView } from "@wizeworks/silicaui-react";
import type { TreeDropEdge, TreeNode } from "@wizeworks/silicaui-react";
import { Section } from "../lib/Section";
const FolderIcon = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
</svg>
);
const FileIcon = (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
<path d="M5 3h9l5 5v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z" />
</svg>
);
const TREE_ITEMS: TreeNode[] = [
{
id: "pages",
label: "Pages",
icon: FolderIcon,
children: [
{ id: "home", label: "Home", icon: FileIcon },
{ id: "about", label: "About", icon: FileIcon },
{ id: "pricing", label: "Pricing", icon: FileIcon },
],
},
{
id: "shop",
label: "Shop",
icon: FolderIcon,
children: [
{
id: "products",
label: "Products",
icon: FolderIcon,
children: [
{ id: "product-list", label: "All products", icon: FileIcon },
{ id: "product-new", label: "New product", icon: FileIcon },
],
},
{ id: "collections", label: "Collections", icon: FileIcon },
{ id: "checkout", label: "Checkout (locked)", icon: FileIcon, disabled: true },
],
},
{ id: "blog", label: "Blog", icon: FileIcon },
{ id: "settings", label: "Settings", icon: FileIcon },
];
/** Remove `id` from the forest, returning the pruned tree + the removed node. */
function removeById(nodes: TreeNode[], id: string): [TreeNode[], TreeNode | undefined] {
let removed: TreeNode | undefined;
const next = nodes.flatMap((n) => {
if (n.id === id) {
removed = n;
return [];
}
if (!n.children) return [n];
const [children, found] = removeById(n.children, id);
if (found) removed = found;
return [{ ...n, children }];
});
return [next, removed];
}
/** Insert `node` before/after/inside `targetId`, wherever it lives in the forest. */
function insertRelative(nodes: TreeNode[], targetId: string, edge: TreeDropEdge, node: TreeNode): TreeNode[] {
const idx = nodes.findIndex((n) => n.id === targetId);
if (idx === -1) {
return nodes.map((n) => (n.children ? { ...n, children: insertRelative(n.children, targetId, edge, node) } : n));
}
if (edge === "inside") {
const target = nodes[idx]!;
const updated = { ...target, children: [...(target.children ?? []), node] };
return [...nodes.slice(0, idx), updated, ...nodes.slice(idx + 1)];
}
const at = edge === "before" ? idx : idx + 1;
return [...nodes.slice(0, at), node, ...nodes.slice(at)];
}
function moveNode(nodes: TreeNode[], id: string, targetId: string, edge: TreeDropEdge): TreeNode[] {
const [without, removed] = removeById(nodes, id);
return removed ? insertRelative(without, targetId, edge, removed) : nodes;
}
export function TreeViewDemo() {
const [selected, setSelected] = useState("about");
const [expanded, setExpanded] = useState<string[]>(["pages", "shop"]);
const [items, setItems] = useState(TREE_ITEMS);
return (
<Section title="Real use · site page tree — drag a row to reorder or reparent it">
<TreeView
items={items}
selected={selected}
onSelectedChange={setSelected}
expanded={expanded}
onExpandedChange={setExpanded}
onMove={(id, targetId, edge) => setItems((prev) => moveNode(prev, id, targetId, edge))}
className="max-w-xs"
/>
</Section>
);
}Vanilla — node tree to HTML
tree — TreeGroup defaults hidden unless defaultExpanded, same convention AccordionPanel/CollapsiblePanel established (the behavior only READS existing hidden state on hydrate, never forces it). Build a node tree, project it with toHtml(), and let the zero-dependency runtime hydrate it. This is the path for generated or user-authored documents — a site builder, CMS output, a static export.
npm i @wizeworks/silicaui-html @wizeworks/silicaui-behaviorsimport { atom, toHtml } from "@wizeworks/silicaui-html";
const node = atom("TreeView", "tree", { /* props */ }, [/* children */]);
const markup = toHtml(node);Styling on this path is the class string in the second argument — the same classes as the Plain HTML section above. There is no color prop here; write "tree tree-primary" rather than passing a colour as a prop.
- Takes children
- yes
- Hydrated by
tree
Load @wizeworks/silicaui-behaviors on the page or this markup renders correctly and does nothing.
The full API for Tree View, on every path, is available through the Silica MCP server.