Componentnavigation1.17 KB gzipZero dependencies
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
Open in StackBlitzimport { TreeNav } from "@/components/ui/tree-nav";
<TreeNav data={[{ id: "orch", label: "Orchestra Tier", children: [{ id: "r1", label: "Row 1" }] }]} />Usage guidance
Use Tree Nav when a ticket stub surface needs Hierarchical explorer with collapsible nodes and icons. Keep props minimal and prefer composition with sibling stubs.
Avoid Tree Nav for long form flows or dense data grids where a dedicated layout block fits better.
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 pe-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 ms-3 space-y-0.5 ps-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[] | |
| selectedId? | string | |
| onSelect? | (node: TreeNode) => void | |
| className? | string |