NavTree

NavTree turns a flat route list into a collapsible sidebar navigation tree.

Basics

A flat list of routes becomes a nested sidebar. The group above the active route is already open, because activeHref opened it.

Loading demo…

import React, { useState } from 'react'; import { NavTree, type NavTreeItem } from '@plocks/ui'; // The whole input: a flat list, with a category on each row. Nothing here // describes the tree — `NavTree` derives it. const ROUTES: NavTreeItem[] = [ { label: 'Getting Started', href: '/getting-started' }, { label: 'Button', href: '/components/Button', group: ['Components', 'Input'] }, { label: 'Select', href: '/components/Select', group: ['Components', 'Input'] }, { label: 'Checkbox', href: '/components/Checkbox', group: ['Components', 'Input'] }, { label: 'Card', href: '/components/Card', group: ['Components', 'Display'] }, { label: 'Badge', href: '/components/Badge', group: ['Components', 'Display'] }, { label: 'Tabs', href: '/components/Tabs', group: ['Components', 'Navigation'] }, ]; export function Demo() { const [route, setRoute] = useState('/components/Select'); return ( <NavTree items={ROUTES} activeHref={route} onNavigate={item => setRoute(item.href)} showGuides /> ); }

Counts and order

groupOrder curates the sections that matter and leaves the rest alphabetical. renderEndSection hangs a count off each branch, and openDepth={0} starts everything closed.

Loading demo…

import React, { useState } from 'react'; import { Badge, NavTree, type NavTreeItem } from '@plocks/ui'; const ROUTES: NavTreeItem[] = [ { label: 'Button', href: '/components/Button', group: 'Input' }, { label: 'Select', href: '/components/Select', group: 'Input' }, { label: 'Checkbox', href: '/components/Checkbox', group: 'Input' }, { label: 'Card', href: '/components/Card', group: 'Display' }, { label: 'Badge', href: '/components/Badge', group: 'Display' }, { label: 'Tabs', href: '/components/Tabs', group: 'Navigation' }, ]; export function Demo() { const [route, setRoute] = useState('/components/Card'); return ( <NavTree items={ROUTES} activeHref={route} onNavigate={item => setRoute(item.href)} // Curate the order that matters and let the rest sort themselves. groupOrder={['Input', 'Display']} openDepth={0} renderEndSection={node => node.children ? <Badge size="xs" variant="light">{node.children.length}</Badge> : null } /> ); }

Filtering

searchable adds a filter field wired to the tree. Typing hides the rows that do not match, opens the branches above the ones that do, and marks the matched substring — past a certain length, three letters beat any amount of nesting.

Loading demo…

import React, { useState } from 'react'; import { NavTree, type NavTreeItem } from '@plocks/ui'; const ROUTES: NavTreeItem[] = [ { label: 'Button', href: '/components/Button', group: 'Input' }, { label: 'Checkbox', href: '/components/Checkbox', group: 'Input' }, { label: 'Select', href: '/components/Select', group: 'Input' }, { label: 'TextArea', href: '/components/TextArea', group: 'Input' }, { label: 'Badge', href: '/components/Badge', group: 'Display' }, { label: 'Card', href: '/components/Card', group: 'Display' }, { label: 'Breadcrumbs', href: '/components/Breadcrumbs', group: 'Navigation' }, { label: 'Tabs', href: '/components/Tabs', group: 'Navigation' }, ]; export function Demo() { const [route, setRoute] = useState('/components/Card'); return ( <NavTree items={ROUTES} activeHref={route} onNavigate={item => setRoute(item.href)} searchable searchPlaceholder="Filter components…" /> ); }

Collapsed rail

collapsed drops the sidebar to a strip of top-level icons — the group holding the current route stays marked, and pressing one lands on the first page inside it. A sidebar with a hundred routes shows a handful of icons here, not a hundred.

Loading demo…

import React, { useState } from 'react'; import { Column, Icon, NavTree, Switch, type NavTreeItem } from '@plocks/ui'; const ROUTES: NavTreeItem[] = [ { label: 'Button', href: '/components/Button', group: 'Components' }, { label: 'Card', href: '/components/Card', group: 'Components' }, { label: 'LineChart', href: '/components/LineChart', group: 'Charts' }, { label: 'BarChart', href: '/components/BarChart', group: 'Charts' }, { label: 'useHover', href: '/hooks/useHover', group: 'Hooks' }, ]; const GROUP_ICONS = { Components: <Icon name="grid" size={18} />, Charts: <Icon name="chart-bar" size={18} />, Hooks: <Icon name="hook" size={18} />, }; export function Demo() { const [collapsed, setCollapsed] = useState(true); const [route, setRoute] = useState('/components/Card'); return ( <Column gap="md"> <Switch checked={collapsed} onChange={setCollapsed} label="Collapsed" /> <NavTree items={ROUTES} activeHref={route} onNavigate={item => setRoute(item.href)} groupIcons={GROUP_ICONS} collapsed={collapsed} /> </Column> ); }
Scan to open this page on your phoneplocks.dev/ui/NavTree