Sidebar
A composable, themeable and customizable sidebar component.
Installation
pnpm dlx shadcn@latest add https://8bit.cnlibs.com/r/sidebar.jsonUsage
"use client";
import {
AudioWaveform,
BadgeCheck,
Bell,
Bot,
ChevronRight,
ChevronsUpDown,
Command,
CreditCard,
Folder,
Forward,
Frame,
GalleryVerticalEnd,
LogOut,
MoreHorizontal,
PieChart,
Plus,
Sparkles,
SquareTerminal,
Trash2,
} from "lucide-react";
import { useCallback, useState } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Separator } from "@/components/ui/separator";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
useSidebar,
} from "@/components/ui/sidebar";
const data = {
user: {
name: "shadcn",
email: "m@example.com",
avatar: "https://github.com/shadcn.png",
},
teams: [
{ name: "Acme Inc", logo: GalleryVerticalEnd, plan: "Enterprise" },
{ name: "Acme Corp.", logo: AudioWaveform, plan: "Startup" },
{ name: "Evil Corp.", logo: Command, plan: "Free" },
],
navMain: [
{
title: "Playground",
icon: SquareTerminal,
isActive: true,
items: [
{ title: "History", url: "#" },
{ title: "Starred", url: "#" },
{ title: "Settings", url: "#" },
],
},
{
title: "Models",
icon: Bot,
items: [
{ title: "Genesis", url: "#" },
{ title: "Explorer", url: "#" },
{ title: "Quantum", url: "#" },
],
},
],
projects: [
{ name: "Design Engineering", url: "#", icon: Frame },
{ name: "Sales & Marketing", url: "#", icon: PieChart },
],
};
function TeamSwitcher() {
const { isMobile } = useSidebar();
const [activeTeam, setActiveTeam] = useState(data.teams[0]);
const handleTeamSelect = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
const name = event.currentTarget.dataset.teamName;
const team = data.teams.find((candidate) => candidate.name === name);
if (team) {
setActiveTeam(team);
}
},
[]
);
if (!activeTeam) {
return null;
}
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
className="data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground"
size="lg"
>
<div className="flex aspect-square size-8 items-center justify-center bg-sidebar-primary px-rounded-sm text-sidebar-primary-foreground [--pixel-size:1px]">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-heading">
{activeTeam.name}
</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
}
/>
<DropdownMenuContent
align="start"
className="w-56"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuGroup>
<DropdownMenuLabel className="text-muted-foreground text-xs">
Teams
</DropdownMenuLabel>
{data.teams.map((team, index) => (
<DropdownMenuItem
className="gap-2 p-2"
data-team-name={team.name}
key={team.name}
onClick={handleTeamSelect}
>
<div className="flex size-6 items-center justify-center px-border-sm px-rounded-sm [--pixel-size:1px]">
<team.logo className="size-3.5 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center px-border-sm px-rounded-sm [--pixel-size:1px]">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">
Add team
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}
function NavMain() {
return (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{data.navMain.map((item) => (
<Collapsible
className="group/collapsible"
defaultOpen={item.isActive}
key={item.title}
render={<SidebarMenuItem />}
>
<CollapsibleTrigger
render={
<SidebarMenuButton tooltip={item.title}>
<item.icon />
<span>{item.title}</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-panel-open/collapsible:rotate-90" />
</SidebarMenuButton>
}
/>
<CollapsibleContent>
<SidebarMenuSub>
{item.items.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton render={<a href={subItem.url} />}>
<span>{subItem.title}</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
);
}
function NavProjects() {
const { isMobile } = useSidebar();
return (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{data.projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton render={<a href={item.url} />}>
<item.icon />
<span>{item.name}</span>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
}
/>
<DropdownMenuContent
align={isMobile ? "end" : "start"}
className="w-48"
side={isMobile ? "bottom" : "right"}
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward className="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<MoreHorizontal className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
);
}
function NavUser() {
const { isMobile } = useSidebar();
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
render={
<SidebarMenuButton
className="data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground"
size="lg"
>
<Avatar className="size-8">
<AvatarImage alt={data.user.name} src={data.user.avatar} />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-heading">
{data.user.name}
</span>
<span className="truncate text-xs">{data.user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
}
/>
<DropdownMenuContent
align="end"
className="w-56"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuGroup>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="size-8">
<AvatarImage alt={data.user.name} src={data.user.avatar} />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-heading">
{data.user.name}
</span>
<span className="truncate text-xs">
{data.user.email}
</span>
</div>
</div>
</DropdownMenuLabel>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}
function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<TeamSwitcher />
</SidebarHeader>
<SidebarContent>
<NavMain />
<NavProjects />
</SidebarContent>
<SidebarFooter>
<NavUser />
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
}<div className="h-[480px] w-full overflow-hidden px-border-sm px-rounded-sm">
<SidebarProvider className="relative h-full min-h-0">
<AppSidebar className="absolute h-auto" />
<SidebarInset>
<header className="flex h-12 shrink-0 items-center gap-2">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator
className="mr-2 data-[orientation=vertical]:h-4"
orientation="vertical"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">
Build Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video bg-muted/50 px-rounded-sm" />
<div className="aspect-video bg-muted/50 px-rounded-sm" />
<div className="aspect-video bg-muted/50 px-rounded-sm" />
</div>
<div className="min-h-24 flex-1 bg-muted/50 px-rounded-sm" />
</div>
</SidebarInset>
</SidebarProvider>
</div>