Componentnavigation
Tree Nav
Hierarchical explorer with collapsible nodes and icons.
Preview
Live Preview
ARENA SEATING ZONES
▶ORCHESTRA TIER
GROUND●Row A (Seats 1-20)
VIP●Row B (Seats 1-20)
▶BALCONY TIER
UPPER●Private Box 1
SOLD●Private Box 2
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add tree-navOr install the full npm package:
npm install bigbulluiUsage
import { TreeNav } from "@/components/ui/tree-nav";
<TreeNav data={[{ id: "orch", label: "Orchestra Tier", children: [{ id: "r1", label: "Row 1" }] }]} />Source code
tree-nav.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface TreeNode {
id: string;
label: string;
badge?: string;
children?: TreeNode[];
}
export interface TreeNavProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {
data: TreeNode[];
selectedId?: string;
onSelect?: (node: TreeNode) => void;
className?: string;
}
function TreeItem({
node,
selectedId,
onSelect,
level = 0,
}: {
node: TreeNode;
selectedId?: string;
onSelect?: (node: TreeNode) => void;
level?: number;
}) {
const [isOpen, setIsOpen] = React.useState(true);
const hasChildren = node.children && node.children.length > 0;
const isSelected = selectedId === node.id;
return (
<div className="space-y-0.5">
<div
onClick={() => {
if (hasChildren) {
setIsOpen(!isOpen);
} else {
onSelect?.(node);
}
}}
style={{ paddingLeft: `${level * 14 + 6}px` }}
className={cn(
"flex cursor-pointer items-center justify-between rounded-md py-1 pr-2 text-xs transition-colors",
isSelected
? "bg-accent font-bold text-accent-foreground shadow-xs"
: "text-foreground hover:bg-secondary"
)}
>
<div className="flex items-center gap-1.5 truncate">
{hasChildren ? (
<span
className={cn(
"size-3.5 flex items-center justify-center text-[10px] text-muted-foreground transition-transform duration-150",
isOpen && "rotate-90"
)}
>
▶
</span>
) : (
<span className="text-[10px] text-accent font-bold">●</span>
)}
<span className="truncate">{node.label}</span>
</div>
{node.badge && (
<span className="rounded-xs bg-secondary/80 px-1 text-[9px] uppercase tracking-wider text-muted-foreground">
{node.badge}
</span>
)}
</div>
{hasChildren && isOpen && (
<div className="relative border-l border-dashed border-border/80 ml-3 space-y-0.5 pl-1 animate-[fade-in_0.12s_ease-out]">
{node.children!.map((child) => (
<TreeItem
key={child.id}
node={child}
selectedId={selectedId}
onSelect={onSelect}
level={level + 1}
/>
))}
</div>
)}
</div>
);
}
export function TreeNav({
data,
selectedId,
onSelect,
className,
...props
}: TreeNavProps) {
return (
<div
className={cn(
"w-full max-w-xs rounded-xl border-2 border-foreground bg-card p-3 shadow-xs font-mono select-none",
className
)}
{...props}
>
<div className="border-b border-dashed border-border pb-2 mb-2 text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
ARENA SEATING ZONES
</div>
<div className="space-y-0.5">
{data.map((node) => (
<TreeItem
key={node.id}
node={node}
selectedId={selectedId}
onSelect={onSelect}
/>
))}
</div>
</div>
);
}
Props
| Prop | Type | Description |
|---|---|---|
| data | TreeNode[] | Hierarchical tree nodes array (required). |
| selectedId | string | Currently selected node ID. |
| onSelect | (node: TreeNode) => void | Callback when a leaf node is selected. |