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
    • 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-react
import { TreeView } from "@wizeworks/silicaui-react";

Props

PropTypeRequiredDescription
itemsTreeNode[]yesThe node forest.
expandedstring[]noControlled set of expanded node ids.
defaultExpandedstring[]noUncontrolled initial expanded ids.
onExpandedChange(expanded: string[]) => voidno—
selectedstringnoControlled selected node id.
defaultSelectedstringnoUncontrolled initial selected id.
onSelectedChange(id: string) => voidno—
onSelect(node: TreeNode) => voidnoFires with the full node when one is selected.
onMove(id: string, targetId: string, edge: TreeDropEdge) => voidnoEnables 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) => voidnoEnables 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-behaviors
import { 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.