All components

Sidebar Tree

A component for displaying a sidebar tree.

Default
Default sidebar tree
1"use client";
2
3import {
4 BookOpen,
5 Box,
6 Component,
7 FormInput,
8 LayoutDashboard,
9 Package,
10} from "lucide-react";
11
12import {
13 SidebarTree,
14 type SidebarTreeSection,
15} from "@/registry/ui/sidebar-tree";
16
17const sections: SidebarTreeSection[] = [
18 {
19 id: "documentation",
20 title: "Documentation",
21 icon: <BookOpen className="size-4" />,
22 defaultOpen: true,
23 items: [
24 {
25 id: "getting-started",
26 label: "Getting started",
27 icon: <LayoutDashboard className="size-4" />,
28 children: [
29 {
30 id: "introduction",
31 label: "Introduction",
32 },
33 {
34 id: "installation",
35 label: "Installation",
36 },
37 ],
38 },
39 {
40 id: "components",
41 label: "Components",
42 icon: <Component className="size-4" />,
43 children: [
44 {
45 id: "forms",
46 label: "Forms",
47 icon: <FormInput className="size-4" />,
48 children: [
49 {
50 id: "input",
51 label: "Input",
52 },
53 {
54 id: "select",
55 label: "Select",
56 badge: "New",
57 },
58 ],
59 },
60 {
61 id: "layout",
62 label: "Layout",
63 icon: <Box className="size-4" />,
64 children: [
65 {
66 id: "sidebar",
67 label: "Sidebar",
68 },
69 {
70 id: "sidebar-tree",
71 label: "Sidebar tree",
72 badge: "New",
73 },
74 ],
75 },
76 ],
77 },
78 ],
79 },
80 {
81 id: "resources",
82 title: "Resources",
83 icon: <Package className="size-4" />,
84 defaultOpen: true,
85 items: [
86 {
87 id: "examples",
88 label: "Examples",
89 children: [
90 {
91 id: "dashboard-example",
92 label: "Dashboard",
93 },
94 {
95 id: "settings-example",
96 label: "Settings",
97 },
98 ],
99 },
100 {
101 id: "templates",
102 label: "Templates",
103 children: [
104 {
105 id: "saas-template",
106 label: "SaaS application",
107 },
108 {
109 id: "admin-template",
110 label: "Admin dashboard",
111 },
112 ],
113 },
114 {
115 id: "changelog",
116 label: "Changelog",
117 },
118 ],
119 },
120];
121
122export function DefaultSidebarTreeExample() {
123 return (
124 <SidebarTree
125 sections={sections}
126 activeId="select"
127 defaultExpandedIds={["components", "forms", "examples"]}
128 onItemSelect={(item) => {
129 console.log("Selected:", item.id);
130 }}
131 className="h-[32rem]"
132 />
133 );
134}
Documentation
Documentation for the sidebar tree

TT UI

Component documentation

Components / Navigation
New

Sidebar tree

A recursive navigation component for presenting nested routes with file-tree connectors and animated expansion states.

1

When to use it

The sidebar tree works well when users need to understand relationships between routes, files, categories, or nested resources.

Documentation navigation
Repository and file browsers
Nested settings areas
Product management hierarchies
Component library navigation
2

Basic usage

Provide recursive tree data and control the selected and expanded states as required.

Example
<SidebarTree
  sections={sidebarSections}
  activeId={activePageId}
  expandedIds={expandedIds}
  onExpandedChange={setExpandedIds}
  openSectionIds={openSectionIds}
  onOpenSectionsChange={setOpenSectionIds}
  onItemSelect={(item) => setActivePageId(item.id)}
/>
3

Controlled expansion

Controlling the expanded IDs allows the application to persist navigation state or synchronise it with the current route.

Example
const [expandedIds, setExpandedIds] = React.useState([
  "components",
  "navigation",
]);

const [openSectionIds, setOpenSectionIds] = React.useState([
  "guides",
  "library",
]);
1"use client";
2
3import * as React from "react";
4import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
5import {
6 Accessibility,
7 ArrowLeft,
8 ArrowRight,
9 Blocks,
10 Box,
11 Braces,
12 Check,
13 Code2,
14 Component,
15 FileCode2,
16 FolderCog,
17 Gauge,
18 LayoutDashboard,
19 MousePointerClick,
20 Package,
21 Palette,
22 Rocket,
23 Settings2,
24 Sparkles,
25 Terminal,
26} from "lucide-react";
27
28import { Badge } from "@/components/ui/badge";
29import { Button } from "@/components/ui/button";
30import {
31 Card,
32 CardContent,
33 CardDescription,
34 CardHeader,
35 CardTitle,
36} from "@/components/ui/card";
37import { Separator } from "@/components/ui/separator";
38import {
39 SidebarTree,
40 type SidebarTreeItem,
41 type SidebarTreeSection,
42} from "@/registry/ui/sidebar-tree";
43
44type ContentSection = {
45 title: string;
46 description: string;
47 paragraphs?: string[];
48 features?: string[];
49 code?: string;
50};
51
52type ContentPage = {
53 id: string;
54 eyebrow: string;
55 title: string;
56 description: string;
57 status?: "Stable" | "Beta" | "New";
58 icon: React.ReactNode;
59 sections: ContentSection[];
60};
61
62const treeItems: SidebarTreeItem[] = [
63 {
64 id: "getting-started",
65 label: "Getting started",
66 icon: <Rocket className="size-4" />,
67 children: [
68 {
69 id: "introduction",
70 label: "Introduction",
71 icon: <FileCode2 className="size-4" />,
72 },
73 {
74 id: "installation",
75 label: "Installation",
76 icon: <Terminal className="size-4" />,
77 },
78 {
79 id: "project-structure",
80 label: "Project structure",
81 icon: <FolderCog className="size-4" />,
82 },
83 ],
84 },
85 {
86 id: "foundations",
87 label: "Foundations",
88 icon: <Palette className="size-4" />,
89 children: [
90 {
91 id: "styling",
92 label: "Styling",
93 icon: <Palette className="size-4" />,
94 },
95 {
96 id: "accessibility",
97 label: "Accessibility",
98 icon: <Accessibility className="size-4" />,
99 },
100 {
101 id: "motion",
102 label: "Motion",
103 icon: <Sparkles className="size-4" />,
104 badge: "New",
105 },
106 {
107 id: "performance",
108 label: "Performance",
109 icon: <Gauge className="size-4" />,
110 },
111 ],
112 },
113 {
114 id: "components",
115 label: "Components",
116 icon: <Component className="size-4" />,
117 children: [
118 {
119 id: "actions",
120 label: "Actions",
121 icon: <MousePointerClick className="size-4" />,
122 children: [
123 {
124 id: "button",
125 label: "Button",
126 },
127 {
128 id: "action-menu",
129 label: "Action menu",
130 },
131 {
132 id: "feedback-trigger",
133 label: "Feedback trigger",
134 badge: "Beta",
135 },
136 ],
137 },
138 {
139 id: "navigation",
140 label: "Navigation",
141 icon: <LayoutDashboard className="size-4" />,
142 children: [
143 {
144 id: "sidebar-tree",
145 label: "Sidebar tree",
146 badge: "New",
147 },
148 {
149 id: "morph-tabs",
150 label: "Morph tabs",
151 },
152 {
153 id: "command-menu",
154 label: "Command menu",
155 },
156 ],
157 },
158 {
159 id: "data-display",
160 label: "Data display",
161 icon: <Braces className="size-4" />,
162 children: [
163 {
164 id: "data-table",
165 label: "Data table",
166 },
167 {
168 id: "status-badge",
169 label: "Status badge",
170 },
171 {
172 id: "timeline",
173 label: "Experience timeline",
174 },
175 ],
176 },
177 ],
178 },
179 {
180 id: "blocks",
181 label: "Blocks",
182 icon: <Blocks className="size-4" />,
183 children: [
184 {
185 id: "dashboard-blocks",
186 label: "Dashboard",
187 icon: <LayoutDashboard className="size-4" />,
188 children: [
189 {
190 id: "analytics-dashboard",
191 label: "Analytics dashboard",
192 },
193 {
194 id: "activity-overview",
195 label: "Activity overview",
196 },
197 ],
198 },
199 {
200 id: "developer-tools",
201 label: "Developer tools",
202 icon: <Code2 className="size-4" />,
203 children: [
204 {
205 id: "api-route-explorer",
206 label: "API route explorer",
207 },
208 {
209 id: "dev-overlay",
210 label: "Developer overlay",
211 },
212 ],
213 },
214 ],
215 },
216 {
217 id: "configuration",
218 label: "Configuration",
219 icon: <Settings2 className="size-4" />,
220 children: [
221 {
222 id: "registry",
223 label: "Registry",
224 icon: <Package className="size-4" />,
225 },
226 {
227 id: "theming",
228 label: "Theming",
229 icon: <Palette className="size-4" />,
230 },
231 {
232 id: "component-api",
233 label: "Component API",
234 icon: <Box className="size-4" />,
235 },
236 ],
237 },
238];
239
240const sidebarSections: SidebarTreeSection[] = [
241 {
242 id: "guides",
243 title: "Guides",
244 icon: <Rocket className="size-4" />,
245 defaultOpen: true,
246 items: treeItems.filter((item) =>
247 ["getting-started", "foundations"].includes(item.id),
248 ),
249 },
250 {
251 id: "library",
252 title: "Library",
253 icon: <Blocks className="size-4" />,
254 defaultOpen: true,
255 items: treeItems.filter((item) =>
256 ["components", "blocks", "configuration"].includes(item.id),
257 ),
258 },
259];
260
261const contentPages: Record<string, ContentPage> = {
262 introduction: {
263 id: "introduction",
264 eyebrow: "Getting started",
265 title: "Introduction",
266 description:
267 "TT UI is a registry-based component library for building polished product interfaces on top of shadcn/ui.",
268 status: "Stable",
269 icon: <Rocket className="size-5" />,
270 sections: [
271 {
272 title: "Built for application interfaces",
273 description:
274 "The library focuses on the UI patterns that repeatedly appear in dashboards, SaaS applications, internal tools, and developer products.",
275 features: [
276 "Copy-owned components installed directly into your project",
277 "Accessible primitives built around familiar shadcn conventions",
278 "Motion used to clarify interaction rather than decorate it",
279 "Composable APIs that can be adapted without fighting the library",
280 ],
281 },
282 {
283 title: "Install a component",
284 description:
285 "Add components through the registry CLI and keep full ownership of the generated source.",
286 code: "npx shadcn@latest add https://tt-ui.dev/r/sidebar-tree.json",
287 },
288 ],
289 },
290 installation: {
291 id: "installation",
292 eyebrow: "Getting started",
293 title: "Installation",
294 description:
295 "Configure your project and install TT UI components through the shadcn registry.",
296 status: "Stable",
297 icon: <Terminal className="size-5" />,
298 sections: [
299 {
300 title: "Requirements",
301 description:
302 "TT UI is designed for modern React applications using Tailwind CSS and shadcn/ui.",
303 features: [
304 "React 18 or later",
305 "Tailwind CSS",
306 "shadcn/ui configuration",
307 "Lucide React",
308 "Motion or Framer Motion for animated components",
309 ],
310 },
311 {
312 title: "Install the component",
313 description:
314 "Run the registry command from the root of your application.",
315 code: "npx shadcn@latest add https://tt-ui.dev/r/sidebar-tree.json",
316 },
317 ],
318 },
319 "project-structure": {
320 id: "project-structure",
321 eyebrow: "Getting started",
322 title: "Project structure",
323 description:
324 "A suggested structure for keeping installed components, blocks, and application code clearly separated.",
325 status: "Stable",
326 icon: <FolderCog className="size-5" />,
327 sections: [
328 {
329 title: "Suggested layout",
330 description:
331 "Registry components can sit alongside shadcn primitives while larger blocks remain separated.",
332 code: `src/
333├── app/
334├── components/
335│ └── ui/
336├── registry/
337│ ├── ui/
338│ ├── blocks/
339│ └── templates/
340└── lib/
341 └── utils.ts`,
342 },
343 ],
344 },
345 motion: {
346 id: "motion",
347 eyebrow: "Foundations",
348 title: "Motion",
349 description:
350 "Animation patterns that preserve context, communicate state changes, and respect reduced-motion preferences.",
351 status: "New",
352 icon: <Sparkles className="size-5" />,
353 sections: [
354 {
355 title: "Motion principles",
356 description:
357 "TT UI uses motion primarily to communicate relationships between states.",
358 features: [
359 "Keep transitions short and interruptible",
360 "Animate opacity and transform where possible",
361 "Avoid motion that blocks interaction",
362 "Respect the operating system reduced-motion preference",
363 ],
364 },
365 {
366 title: "Reduced motion",
367 description:
368 "Use the Motion preference hook to remove non-essential transitions.",
369 code: `const shouldReduceMotion = useReducedMotion();
370
371const transition = shouldReduceMotion
372 ? { duration: 0 }
373 : { duration: 0.22, ease: [0.22, 1, 0.36, 1] };`,
374 },
375 ],
376 },
377 accessibility: {
378 id: "accessibility",
379 eyebrow: "Foundations",
380 title: "Accessibility",
381 description:
382 "Structural and interaction guidance for building components that remain usable across input methods.",
383 status: "Stable",
384 icon: <Accessibility className="size-5" />,
385 sections: [
386 {
387 title: "Interaction requirements",
388 description:
389 "Interactive components should expose their state and remain operable without a pointer.",
390 features: [
391 "Use native interactive elements where possible",
392 "Provide visible keyboard focus states",
393 "Expose expanded and selected states",
394 "Preserve logical focus order",
395 "Avoid relying on colour alone to convey meaning",
396 ],
397 },
398 ],
399 },
400 "sidebar-tree": {
401 id: "sidebar-tree",
402 eyebrow: "Components / Navigation",
403 title: "Sidebar tree",
404 description:
405 "A recursive navigation component for presenting nested routes with file-tree connectors and animated expansion states.",
406 status: "New",
407 icon: <LayoutDashboard className="size-5" />,
408 sections: [
409 {
410 title: "When to use it",
411 description:
412 "The sidebar tree works well when users need to understand relationships between routes, files, categories, or nested resources.",
413 features: [
414 "Documentation navigation",
415 "Repository and file browsers",
416 "Nested settings areas",
417 "Product management hierarchies",
418 "Component library navigation",
419 ],
420 },
421 {
422 title: "Basic usage",
423 description:
424 "Provide recursive tree data and control the selected and expanded states as required.",
425 code: `<SidebarTree
426 sections={sidebarSections}
427 activeId={activePageId}
428 expandedIds={expandedIds}
429 onExpandedChange={setExpandedIds}
430 openSectionIds={openSectionIds}
431 onOpenSectionsChange={setOpenSectionIds}
432 onItemSelect={(item) => setActivePageId(item.id)}
433/>`,
434 },
435 {
436 title: "Controlled expansion",
437 description:
438 "Controlling the expanded IDs allows the application to persist navigation state or synchronise it with the current route.",
439 code: `const [expandedIds, setExpandedIds] = React.useState([
440 "components",
441 "navigation",
442]);
443
444const [openSectionIds, setOpenSectionIds] = React.useState([
445 "guides",
446 "library",
447]);`,
448 },
449 ],
450 },
451 "data-table": {
452 id: "data-table",
453 eyebrow: "Components / Data display",
454 title: "Data table",
455 description:
456 "A flexible table layer for sorting, filtering, pagination, selection, and dense application data.",
457 status: "Beta",
458 icon: <Braces className="size-5" />,
459 sections: [
460 {
461 title: "Designed for real data",
462 description:
463 "The table supports long values, empty states, loading states, responsive overflow, and application-level controls.",
464 features: [
465 "TanStack Table integration",
466 "Column sorting and filtering",
467 "Row selection",
468 "Pagination controls",
469 "Custom cell renderers",
470 ],
471 },
472 ],
473 },
474 "api-route-explorer": {
475 id: "api-route-explorer",
476 eyebrow: "Blocks / Developer tools",
477 title: "API route explorer",
478 description:
479 "An application block for browsing endpoints, configuring requests, and inspecting structured API responses.",
480 status: "Beta",
481 icon: <Code2 className="size-5" />,
482 sections: [
483 {
484 title: "Included surfaces",
485 description:
486 "The block combines navigation, request configuration, response inspection, and endpoint metadata.",
487 features: [
488 "Nested endpoint navigation",
489 "Method and status indicators",
490 "Request headers and body editors",
491 "Response preview",
492 "Responsive application layout",
493 ],
494 },
495 ],
496 },
497 registry: {
498 id: "registry",
499 eyebrow: "Configuration",
500 title: "Registry",
501 description:
502 "Configure shadcn to install TT UI components directly into your application.",
503 status: "Stable",
504 icon: <Package className="size-5" />,
505 sections: [
506 {
507 title: "Registry installation",
508 description:
509 "Each component is delivered as source code rather than an opaque runtime package.",
510 code: "npx shadcn@latest add https://tt-ui.dev/r/sidebar-tree.json",
511 },
512 ],
513 },
514};
515
516const fallbackPage: ContentPage = {
517 id: "fallback",
518 eyebrow: "TT UI",
519 title: "Component documentation",
520 description:
521 "Select a documentation page from the tree to inspect its overview, API, examples, and implementation guidance.",
522 icon: <Component className="size-5" />,
523 sections: [
524 {
525 title: "Explore the library",
526 description:
527 "The navigation supports deeply nested sections while preserving the relationship between parent and child items.",
528 features: [
529 "Getting started guides",
530 "Design and accessibility foundations",
531 "Components and blocks",
532 "Registry configuration",
533 ],
534 },
535 ],
536};
537
538const pageOrder = [
539 "introduction",
540 "installation",
541 "project-structure",
542 "accessibility",
543 "motion",
544 "sidebar-tree",
545 "data-table",
546 "api-route-explorer",
547 "registry",
548];
549
550export function SidebarTreeDocumentationExample() {
551 const shouldReduceMotion = useReducedMotion();
552
553 const [activePageId, setActivePageId] = React.useState("sidebar-tree");
554 const [expandedIds, setExpandedIds] = React.useState<string[]>([
555 "components",
556 "navigation",
557 "getting-started",
558 ]);
559 const [openSectionIds, setOpenSectionIds] = React.useState<string[]>([
560 "guides",
561 "library",
562 ]);
563
564 const activePage = contentPages[activePageId] ?? fallbackPage;
565 const activePageIndex = pageOrder.indexOf(activePageId);
566
567 const previousPageId =
568 activePageIndex > 0 ? pageOrder[activePageIndex - 1] : undefined;
569
570 const nextPageId =
571 activePageIndex >= 0 && activePageIndex < pageOrder.length - 1
572 ? pageOrder[activePageIndex + 1]
573 : undefined;
574
575 function navigateToPage(pageId: string | undefined) {
576 if (!pageId) return;
577
578 setActivePageId(pageId);
579 ensureItemVisible(pageId);
580 }
581
582 function ensureItemVisible(pageId: string) {
583 const location = findTreeLocation(sidebarSections, pageId);
584
585 if (!location) return;
586
587 setExpandedIds((current) => [
588 ...new Set([...current, ...location.parentIds]),
589 ]);
590
591 setOpenSectionIds((current) => [
592 ...new Set([...current, location.sectionId]),
593 ]);
594 }
595
596 return (
597 <div className="min-h-screen w-full min-w-0 bg-muted/30">
598 <header className="border-b bg-background">
599 <div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6">
600 <div className="flex items-center gap-3">
601 <div className="flex size-9 items-center justify-center rounded-lg bg-foreground text-background">
602 <Component className="size-4" />
603 </div>
604
605 <div>
606 <p className="text-sm font-semibold">TT UI</p>
607 <p className="text-xs text-muted-foreground">
608 Component documentation
609 </p>
610 </div>
611 </div>
612
613 <div className="flex items-center gap-2">
614 <Button variant="ghost" size="sm">
615 Components
616 </Button>
617
618 <Button size="sm">
619 <Package className="size-4" />
620 Open registry
621 </Button>
622 </div>
623 </div>
624 </header>
625
626 <main className="grid w-full min-w-0 items-start gap-6 px-4 py-6 sm:px-6 lg:grid-cols-[300px_minmax(0,1fr)]">
627 <aside className="w-full min-w-0 lg:sticky lg:top-6 lg:w-[300px] lg:self-start">
628 <SidebarTree
629 sections={sidebarSections}
630 ariaLabel="TT UI documentation navigation"
631 activeId={activePageId}
632 expandedIds={expandedIds}
633 onExpandedChange={setExpandedIds}
634 openSectionIds={openSectionIds}
635 onOpenSectionsChange={setOpenSectionIds}
636 onItemSelect={(item) => navigateToPage(item.id)}
637 className="h-[32rem] max-w-none bg-background"
638 />
639
640 <Card className="mt-4 w-full">
641 <CardHeader className="pb-3">
642 <CardTitle className="text-sm">Library status</CardTitle>
643 <CardDescription>Current registry coverage.</CardDescription>
644 </CardHeader>
645
646 <CardContent className="space-y-3 text-sm">
647 <StatusRow label="Components" value="42" />
648 <StatusRow label="Blocks" value="18" />
649 <StatusRow label="Templates" value="7" />
650 </CardContent>
651 </Card>
652 </aside>
653
654 <section className="w-full min-w-0">
655 {/* Keep this width-defining shell mounted between page changes. */}
656 <article className="w-full min-w-0 overflow-hidden rounded-xl border bg-background">
657 <AnimatePresence mode="wait" initial={false}>
658 <motion.div
659 key={activePage.id}
660 initial={
661 shouldReduceMotion
662 ? false
663 : {
664 opacity: 0,
665 y: 8,
666 }
667 }
668 animate={{
669 opacity: 1,
670 y: 0,
671 }}
672 exit={
673 shouldReduceMotion
674 ? undefined
675 : {
676 opacity: 0,
677 y: -6,
678 }
679 }
680 transition={{
681 duration: shouldReduceMotion ? 0 : 0.18,
682 ease: "easeOut",
683 }}
684 className="w-full min-w-0"
685 >
686 <div className="w-full min-w-0 border-b px-6 py-8 sm:px-8 sm:py-10">
687 <div className="mb-6 flex min-w-0 items-center justify-between gap-4">
688 <div className="flex min-w-0 items-center gap-2 text-sm text-muted-foreground">
689 <span className="shrink-0">{activePage.icon}</span>
690 <span className="truncate">{activePage.eyebrow}</span>
691 </div>
692
693 {activePage.status && (
694 <div className="shrink-0">
695 <StatusBadge status={activePage.status} />
696 </div>
697 )}
698 </div>
699
700 <div className="w-full min-w-0 max-w-3xl">
701 <h1 className="break-words text-3xl font-semibold tracking-tight sm:text-4xl">
702 {activePage.title}
703 </h1>
704
705 <p className="mt-4 break-words text-base leading-7 text-muted-foreground sm:text-lg">
706 {activePage.description}
707 </p>
708 </div>
709 </div>
710
711 <div className="w-full min-w-0 space-y-10 px-6 py-8 sm:px-8 sm:py-10">
712 {activePage.sections.map((section, index) => (
713 <section
714 key={`${activePage.id}-${section.title}`}
715 className="w-full min-w-0 max-w-4xl"
716 >
717 <div className="flex min-w-0 gap-4">
718 <div className="flex size-7 shrink-0 items-center justify-center rounded-full border bg-muted text-xs font-medium">
719 {index + 1}
720 </div>
721
722 <div className="w-full min-w-0 flex-1">
723 <h2 className="break-words text-xl font-semibold tracking-tight">
724 {section.title}
725 </h2>
726
727 <p className="mt-2 break-words leading-7 text-muted-foreground">
728 {section.description}
729 </p>
730
731 {section.paragraphs?.map((paragraph) => (
732 <p
733 key={paragraph}
734 className="mt-4 break-words leading-7 text-muted-foreground"
735 >
736 {paragraph}
737 </p>
738 ))}
739
740 {section.features && (
741 <div className="mt-5 grid w-full min-w-0 gap-3 sm:grid-cols-2">
742 {section.features.map((feature) => (
743 <div
744 key={feature}
745 className="flex min-w-0 items-start gap-3 rounded-lg border bg-muted/30 p-3"
746 >
747 <div className="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-foreground text-background">
748 <Check className="size-3" />
749 </div>
750
751 <span className="min-w-0 break-words text-sm leading-5">
752 {feature}
753 </span>
754 </div>
755 ))}
756 </div>
757 )}
758
759 {section.code && (
760 <div className="mt-5 w-full min-w-0 max-w-full overflow-hidden rounded-lg border bg-zinc-950">
761 <div className="flex min-w-0 items-center justify-between border-b border-white/10 px-4 py-2">
762 <span className="text-xs text-zinc-400">
763 Example
764 </span>
765
766 <Button
767 variant="ghost"
768 size="sm"
769 className="h-7 shrink-0 text-zinc-400 hover:bg-white/10 hover:text-white"
770 onClick={() =>
771 navigator.clipboard.writeText(
772 section.code ?? "",
773 )
774 }
775 >
776 Copy
777 </Button>
778 </div>
779
780 <pre className="max-w-full overflow-x-auto p-4 text-sm leading-6 text-zinc-100">
781 <code>{section.code}</code>
782 </pre>
783 </div>
784 )}
785 </div>
786 </div>
787 </section>
788 ))}
789 </div>
790
791 <Separator />
792
793 <footer className="grid w-full min-w-0 gap-3 p-6 sm:grid-cols-2 sm:p-8">
794 {previousPageId ? (
795 <PageNavigationButton
796 direction="previous"
797 page={contentPages[previousPageId]}
798 onClick={() => navigateToPage(previousPageId)}
799 />
800 ) : (
801 <div aria-hidden="true" />
802 )}
803
804 {nextPageId && (
805 <PageNavigationButton
806 direction="next"
807 page={contentPages[nextPageId]}
808 onClick={() => navigateToPage(nextPageId)}
809 />
810 )}
811 </footer>
812 </motion.div>
813 </AnimatePresence>
814 </article>
815 </section>
816 </main>
817 </div>
818 );
819}
820
821function StatusRow({ label, value }: { label: string; value: string }) {
822 return (
823 <div className="flex items-center justify-between">
824 <span className="text-muted-foreground">{label}</span>
825 <span className="font-medium">{value}</span>
826 </div>
827 );
828}
829
830function StatusBadge({
831 status,
832}: {
833 status: NonNullable<ContentPage["status"]>;
834}) {
835 const variant =
836 status === "Stable"
837 ? "secondary"
838 : status === "New"
839 ? "default"
840 : "outline";
841
842 return <Badge variant={variant}>{status}</Badge>;
843}
844
845function PageNavigationButton({
846 page,
847 direction,
848 onClick,
849}: {
850 page: ContentPage;
851 direction: "previous" | "next";
852 onClick: () => void;
853}) {
854 const isNext = direction === "next";
855
856 return (
857 <button
858 type="button"
859 onClick={onClick}
860 className={[
861 "group flex min-h-20 items-center gap-4 rounded-lg border p-4",
862 "text-left transition-colors hover:bg-muted/50",
863 "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
864 isNext ? "justify-between sm:text-right" : "",
865 ].join(" ")}
866 >
867 {!isNext && (
868 <ArrowLeft className="size-4 shrink-0 text-muted-foreground transition-transform group-hover:-translate-x-1" />
869 )}
870
871 <div className={isNext ? "ml-auto" : undefined}>
872 <p className="text-xs text-muted-foreground">
873 {isNext ? "Next" : "Previous"}
874 </p>
875 <p className="mt-1 font-medium">{page.title}</p>
876 </div>
877
878 {isNext && (
879 <ArrowRight className="size-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-1" />
880 )}
881 </button>
882 );
883}
884
885type TreeLocation = {
886 sectionId: string;
887 parentIds: string[];
888};
889
890function findTreeLocation(
891 sections: SidebarTreeSection[],
892 targetId: string,
893): TreeLocation | undefined {
894 for (const section of sections) {
895 const parentIds = findItemParentIds(section.items, targetId);
896
897 if (parentIds !== undefined) {
898 return {
899 sectionId: section.id,
900 parentIds,
901 };
902 }
903 }
904
905 return undefined;
906}
907
908function findItemParentIds(
909 items: SidebarTreeItem[],
910 targetId: string,
911 parentIds: string[] = [],
912): string[] | undefined {
913 for (const item of items) {
914 if (item.id === targetId) {
915 return parentIds;
916 }
917
918 if (item.children?.length) {
919 const result = findItemParentIds(item.children, targetId, [
920 ...parentIds,
921 item.id,
922 ]);
923
924 if (result !== undefined) {
925 return result;
926 }
927 }
928 }
929
930 return undefined;
931}
Other Examples
Other examples of the sidebar tree
Fixed height
Viewport relative
1"use client";
2
3import * as React from "react";
4import {
5 Accessibility,
6 Blocks,
7 Box,
8 Braces,
9 Code2,
10 Component,
11 FileCode2,
12 FolderCog,
13 Gauge,
14 LayoutDashboard,
15 MousePointerClick,
16 Package,
17 Palette,
18 Rocket,
19 Settings2,
20 Sparkles,
21 Terminal,
22} from "lucide-react";
23
24import {
25 SidebarTree,
26 type SidebarTreeItem,
27 type SidebarTreeSection,
28} from "@/registry/ui/sidebar-tree";
29import { cn } from "@/lib/utils";
30
31const treeItems: SidebarTreeItem[] = [
32 {
33 id: "getting-started",
34 label: "Getting started",
35 icon: <Rocket className="size-4" />,
36 children: [
37 {
38 id: "introduction",
39 label: "Introduction",
40 icon: <FileCode2 className="size-4" />,
41 },
42 {
43 id: "installation",
44 label: "Installation",
45 icon: <Terminal className="size-4" />,
46 },
47 {
48 id: "project-structure",
49 label: "Project structure",
50 icon: <FolderCog className="size-4" />,
51 },
52 ],
53 },
54 {
55 id: "foundations",
56 label: "Foundations",
57 icon: <Palette className="size-4" />,
58 children: [
59 {
60 id: "styling",
61 label: "Styling",
62 icon: <Palette className="size-4" />,
63 },
64 {
65 id: "accessibility",
66 label: "Accessibility",
67 icon: <Accessibility className="size-4" />,
68 },
69 {
70 id: "motion",
71 label: "Motion",
72 icon: <Sparkles className="size-4" />,
73 badge: "New",
74 },
75 {
76 id: "performance",
77 label: "Performance",
78 icon: <Gauge className="size-4" />,
79 },
80 ],
81 },
82 {
83 id: "components",
84 label: "Components",
85 icon: <Component className="size-4" />,
86 children: [
87 {
88 id: "actions",
89 label: "Actions",
90 icon: <MousePointerClick className="size-4" />,
91 children: [
92 {
93 id: "button",
94 label: "Button",
95 },
96 {
97 id: "action-menu",
98 label: "Action menu",
99 },
100 {
101 id: "feedback-trigger",
102 label: "Feedback trigger",
103 badge: "Beta",
104 },
105 ],
106 },
107 {
108 id: "navigation",
109 label: "Navigation",
110 icon: <LayoutDashboard className="size-4" />,
111 children: [
112 {
113 id: "sidebar-tree",
114 label: "Sidebar tree",
115 badge: "New",
116 },
117 {
118 id: "morph-tabs",
119 label: "Morph tabs",
120 },
121 {
122 id: "command-menu",
123 label: "Command menu",
124 },
125 ],
126 },
127 {
128 id: "data-display",
129 label: "Data display",
130 icon: <Braces className="size-4" />,
131 children: [
132 {
133 id: "data-table",
134 label: "Data table",
135 },
136 {
137 id: "status-badge",
138 label: "Status badge",
139 },
140 {
141 id: "timeline",
142 label: "Experience timeline",
143 },
144 ],
145 },
146 ],
147 },
148 {
149 id: "blocks",
150 label: "Blocks",
151 icon: <Blocks className="size-4" />,
152 children: [
153 {
154 id: "dashboard-blocks",
155 label: "Dashboard",
156 icon: <LayoutDashboard className="size-4" />,
157 children: [
158 {
159 id: "analytics-dashboard",
160 label: "Analytics dashboard",
161 },
162 {
163 id: "activity-overview",
164 label: "Activity overview",
165 },
166 ],
167 },
168 {
169 id: "developer-tools",
170 label: "Developer tools",
171 icon: <Code2 className="size-4" />,
172 children: [
173 {
174 id: "api-route-explorer",
175 label: "API route explorer",
176 },
177 {
178 id: "dev-overlay",
179 label: "Developer overlay",
180 },
181 ],
182 },
183 ],
184 },
185 {
186 id: "configuration",
187 label: "Configuration",
188 icon: <Settings2 className="size-4" />,
189 children: [
190 {
191 id: "registry",
192 label: "Registry",
193 icon: <Package className="size-4" />,
194 },
195 {
196 id: "theming",
197 label: "Theming",
198 icon: <Palette className="size-4" />,
199 },
200 {
201 id: "component-api",
202 label: "Component API",
203 icon: <Box className="size-4" />,
204 },
205 ],
206 },
207];
208
209const sections: SidebarTreeSection[] = [
210 {
211 id: "guides",
212 title: "Guides",
213 icon: <Rocket className="size-4" />,
214 defaultOpen: true,
215 items: treeItems.filter((item) =>
216 ["getting-started", "foundations"].includes(item.id),
217 ),
218 },
219 {
220 id: "library",
221 title: "Library",
222 icon: <Blocks className="size-4" />,
223 defaultOpen: true,
224 items: treeItems.filter((item) =>
225 ["components", "blocks", "configuration"].includes(item.id),
226 ),
227 },
228];
229
230export function OtherSidebarTreeExamples() {
231 const [activePageId, setActivePageId] = React.useState("sidebar-tree");
232
233 const [expandedIds, setExpandedIds] = React.useState<string[]>([
234 "getting-started",
235 "components",
236 "navigation",
237 ]);
238
239 const [openSectionIds, setOpenSectionIds] = React.useState<string[]>([
240 "guides",
241 "library",
242 ]);
243
244 function handleItemSelect(item: SidebarTreeItem) {
245 setActivePageId(item.id);
246 ensureItemVisible(item.id);
247 }
248
249 function ensureItemVisible(itemId: string) {
250 const location = findTreeLocation(sections, itemId);
251
252 if (!location) return;
253
254 setExpandedIds((current) => [
255 ...new Set([...current, ...location.parentIds]),
256 ]);
257
258 setOpenSectionIds((current) => [
259 ...new Set([...current, location.sectionId]),
260 ]);
261 }
262
263 const sharedProps = {
264 sections,
265 activeId: activePageId,
266 expandedIds,
267 onExpandedChange: setExpandedIds,
268 openSectionIds,
269 onOpenSectionsChange: setOpenSectionIds,
270 onItemSelect: handleItemSelect,
271 };
272
273 return (
274 <div className="grid min-h-screen w-full min-w-0 grid-cols-1 gap-4 bg-muted/30 lg:grid-cols-2">
275 <div className="flex min-w-0 flex-col gap-2">
276 <span className="text-sm font-medium">Fixed height</span>
277
278 <SidebarTree
279 {...sharedProps}
280 ariaLabel="Fixed-height documentation navigation"
281 className="h-[36rem] max-w-none bg-background"
282 />
283 </div>
284
285 <div className="flex min-w-0 flex-col gap-2">
286 <span className="text-sm font-medium">Viewport relative</span>
287
288 <SidebarTree
289 {...sharedProps}
290 ariaLabel="Viewport-relative documentation navigation"
291 className={cn(
292 "h-[30rem] max-w-none bg-background",
293 "lg:h-[calc(100dvh-8rem)] lg:max-h-[44rem]",
294 )}
295 />
296 </div>
297 </div>
298 );
299}
300
301type TreeLocation = {
302 sectionId: string;
303 parentIds: string[];
304};
305
306function findTreeLocation(
307 sections: SidebarTreeSection[],
308 targetId: string,
309): TreeLocation | undefined {
310 for (const section of sections) {
311 const parentIds = findItemParentIds(section.items, targetId);
312
313 if (parentIds !== undefined) {
314 return {
315 sectionId: section.id,
316 parentIds,
317 };
318 }
319 }
320
321 return undefined;
322}
323
324function findItemParentIds(
325 items: SidebarTreeItem[],
326 targetId: string,
327 parentIds: string[] = [],
328): string[] | undefined {
329 for (const item of items) {
330 if (item.id === targetId) {
331 return parentIds;
332 }
333
334 if (item.children?.length) {
335 const result = findItemParentIds(item.children, targetId, [
336 ...parentIds,
337 item.id,
338 ]);
339
340 if (result !== undefined) {
341 return result;
342 }
343 }
344 }
345
346 return undefined;
347}

Installation & source

Install via the shadcn CLI or copy the registry files manually.

bash
npx shadcn@latest add @tt-ui/sidebar-tree

Props

NameTypeDefaultDescription
sectionsSidebarTreeSection[]undefinedThe sections to display in the sidebar tree
ariaLabelstringundefinedThe aria label of the sidebar tree
activeIdstringundefinedThe active section id
defaultExpandedIdsstring[]undefinedThe default expanded item ids
expandedIdsstring[]undefinedThe expanded section ids
onExpandedChange(expandedIds: string[]) => voidundefinedCalled when the expanded section ids change
openSectionIdsstring[]undefinedThe open section ids
onOpenSectionsChange(openSectionIds: string[]) => voidundefinedCalled when the open section ids change
onItemSelect(item: SidebarTreeItem) => voidundefinedCalled when an item is selected
classNamestringundefinedAdditional classes on the sidebar tree wrapper
viewportClassNamestringundefinedAdditional classes on the viewport wrapper