Skip to content

shadcn.io is not affiliated with official shadcn/ui

Shadcn Sidebar for React

A sidebar for navigation and app structure.

Installation

bunx --bun shadcn@latest add https://kit.dev/r/base/sidebar.json

Anatomy

Usage

import {
  AudioWaveformIcon,
  BadgeCheckIcon,
  BellIcon,
  BookOpenIcon,
  BotIcon,
  ChevronRightIcon,
  ChevronsUpDownIcon,
  CommandIcon,
  CreditCardIcon,
  FolderIcon,
  ForwardIcon,
  FrameIcon,
  GalleryVerticalEndIcon,
  LogOutIcon,
  MapIcon,
  MoreHorizontalIcon,
  PieChartIcon,
  PlusIcon,
  Settings2Icon,
  SparklesIcon,
  SquareTerminalIcon,
  Trash2Icon,
} from "lucide-react";
import { type ElementType, useCallback, useState } from "react";
import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/avatar";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
  Menu,
  MenuContent,
  MenuGroup,
  MenuGroupLabel,
  MenuItem,
  MenuSeparator,
  MenuShortcut,
  MenuTrigger,
} from "@/components/ui/menu";
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 = {
  navMain: [
    {
      icon: SquareTerminalIcon,
      isActive: true,
      items: [
        { title: "History", url: "#" },
        { title: "Starred", url: "#" },
        { title: "Settings", url: "#" },
      ],
      title: "Playground",
      url: "#",
    },
    {
      icon: BotIcon,
      items: [
        { title: "Genesis", url: "#" },
        { title: "Explorer", url: "#" },
        { title: "Quantum", url: "#" },
      ],
      title: "Models",
      url: "#",
    },
    {
      icon: BookOpenIcon,
      items: [
        { title: "Introduction", url: "#" },
        { title: "Get Started", url: "#" },
        { title: "Tutorials", url: "#" },
        { title: "Changelog", url: "#" },
      ],
      title: "Documentation",
      url: "#",
    },
    {
      icon: Settings2Icon,
      items: [
        { title: "General", url: "#" },
        { title: "Team", url: "#" },
        { title: "Billing", url: "#" },
        { title: "Limits", url: "#" },
      ],
      title: "Settings",
      url: "#",
    },
  ],
  projects: [
    { icon: FrameIcon, name: "Design Engineering", url: "#" },
    { icon: PieChartIcon, name: "Sales & Marketing", url: "#" },
    { icon: MapIcon, name: "Travel", url: "#" },
  ],
  teams: [
    {
      logo: GalleryVerticalEndIcon,
      name: "Acme Inc",
      plan: "Enterprise",
    },
    {
      logo: AudioWaveformIcon,
      name: "Acme Corp.",
      plan: "Startup",
    },
    {
      logo: CommandIcon,
      name: "Evil Corp.",
      plan: "Free",
    },
  ],
  user: {
    avatar: "https://github.com/shadcn.png",
    email: "[email protected]",
    name: "shadcn",
  },
};

type Team = (typeof data.teams)[number];

const TeamMenuItem = (props: {
  index: number;
  onSelect: (team: Team) => void;
  team: Team;
}) => {
  const { index, onSelect, team } = props;
  const handleClick = useCallback(() => {
    onSelect(team);
  }, [onSelect, team]);

  return (
    <MenuItem className="gap-2 p-2" onClick={handleClick} value={team.name}>
      <div className="flex size-6 items-center justify-center rounded-md border">
        <team.logo aria-hidden="true" className="size-3.5 shrink-0" />
      </div>
      {team.name}
      <MenuShortcut>⌘{index + 1}</MenuShortcut>
    </MenuItem>
  );
};

const TeamSwitcher = (props: { teams: Team[] }) => {
  const { teams } = props;
  const { isMobile } = useSidebar();
  const [activeTeam, setActiveTeam] = useState(teams[0]);

  const handleSelectTeam = useCallback((team: Team) => {
    setActiveTeam(team);
  }, []);

  if (!activeTeam) {
    return null;
  }

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <Menu
          positioning={{
            gutter: 4,
            placement: isMobile ? "bottom-start" : "right-start",
          }}
        >
          <MenuTrigger asChild>
            <SidebarMenuButton
              className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
              size="lg"
            >
              <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
                <activeTeam.logo aria-hidden="true" className="size-4" />
              </div>
              <div className="grid flex-1 text-start text-sm leading-tight">
                <span className="truncate font-medium">{activeTeam.name}</span>
                <span className="truncate text-xs">{activeTeam.plan}</span>
              </div>
              <ChevronsUpDownIcon aria-hidden="true" className="ms-auto" />
            </SidebarMenuButton>
          </MenuTrigger>
          <MenuContent className="w-(--reference-width) min-w-56 rounded-lg">
            <MenuGroup>
              <MenuGroupLabel className="text-muted-foreground text-xs">
                Teams
              </MenuGroupLabel>
              {teams.map((team, index) => (
                <TeamMenuItem
                  index={index}
                  key={team.name}
                  onSelect={handleSelectTeam}
                  team={team}
                />
              ))}
            </MenuGroup>
            <MenuSeparator />
            <MenuGroup>
              <MenuItem className="gap-2 p-2" value="add-team">
                <div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
                  <PlusIcon aria-hidden="true" className="size-4" />
                </div>
                <div className="font-medium text-muted-foreground">
                  Add team
                </div>
              </MenuItem>
            </MenuGroup>
          </MenuContent>
        </Menu>
      </SidebarMenuItem>
    </SidebarMenu>
  );
};

const NavMain = (props: {
  items: {
    icon?: ElementType;
    isActive?: boolean;
    items?: { title: string; url: string }[];
    title: string;
    url: string;
  }[];
}) => {
  const { items } = props;

  return (
    <SidebarGroup>
      <SidebarGroupLabel>Platform</SidebarGroupLabel>
      <SidebarMenu>
        {items.map((item) => (
          <Collapsible
            asChild
            className="group/collapsible"
            defaultOpen={item.isActive}
            key={item.title}
            lazyMount={false}
            unmountOnExit={false}
          >
            <SidebarMenuItem>
              <CollapsibleTrigger asChild>
                <SidebarMenuButton tooltip={item.title}>
                  {item.icon ? <item.icon aria-hidden="true" /> : null}
                  <span>{item.title}</span>
                  <ChevronRightIcon
                    aria-hidden="true"
                    className="ms-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
                  />
                </SidebarMenuButton>
              </CollapsibleTrigger>
              <CollapsibleContent>
                <SidebarMenuSub>
                  {item.items?.map((subItem) => (
                    <SidebarMenuSubItem key={subItem.title}>
                      <SidebarMenuSubButton href={subItem.url}>
                        <span>{subItem.title}</span>
                      </SidebarMenuSubButton>
                    </SidebarMenuSubItem>
                  ))}
                </SidebarMenuSub>
              </CollapsibleContent>
            </SidebarMenuItem>
          </Collapsible>
        ))}
      </SidebarMenu>
    </SidebarGroup>
  );
};

const NavProjects = (props: {
  projects: { icon: ElementType; name: string; url: string }[];
}) => {
  const { projects } = props;
  const { isMobile } = useSidebar();

  return (
    <SidebarGroup className="group-data-[collapsible=icon]:hidden">
      <SidebarGroupLabel>Projects</SidebarGroupLabel>
      <SidebarMenu>
        {projects.map((item) => (
          <SidebarMenuItem key={item.name}>
            <SidebarMenuButton asChild>
              <a href={item.url}>
                <item.icon aria-hidden="true" />
                <span>{item.name}</span>
              </a>
            </SidebarMenuButton>
            <Menu
              positioning={{
                placement: isMobile ? "bottom-end" : "right-start",
              }}
            >
              <MenuTrigger asChild>
                <SidebarMenuAction showOnHover>
                  <MoreHorizontalIcon aria-hidden="true" />
                  <span className="sr-only">More</span>
                </SidebarMenuAction>
              </MenuTrigger>
              <MenuContent className="w-48 rounded-lg">
                <MenuItem value={`${item.name}-view`}>
                  <FolderIcon
                    aria-hidden="true"
                    className="text-muted-foreground"
                  />
                  <span>View Project</span>
                </MenuItem>
                <MenuItem value={`${item.name}-share`}>
                  <ForwardIcon
                    aria-hidden="true"
                    className="text-muted-foreground"
                  />
                  <span>Share Project</span>
                </MenuItem>
                <MenuSeparator />
                <MenuItem value={`${item.name}-delete`}>
                  <Trash2Icon
                    aria-hidden="true"
                    className="text-muted-foreground"
                  />
                  <span>Delete Project</span>
                </MenuItem>
              </MenuContent>
            </Menu>
          </SidebarMenuItem>
        ))}
        <SidebarMenuItem>
          <SidebarMenuButton className="text-sidebar-foreground/70">
            <MoreHorizontalIcon
              aria-hidden="true"
              className="text-sidebar-foreground/70"
            />
            <span>More</span>
          </SidebarMenuButton>
        </SidebarMenuItem>
      </SidebarMenu>
    </SidebarGroup>
  );
};

const NavUser = (props: {
  user: { avatar: string; email: string; name: string };
}) => {
  const { user } = props;
  const { isMobile } = useSidebar();

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <Menu
          positioning={{
            gutter: 4,
            placement: isMobile ? "bottom-end" : "right-end",
          }}
        >
          <MenuTrigger asChild>
            <SidebarMenuButton
              className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
              size="lg"
            >
              <Avatar className="size-8 rounded-lg">
                <AvatarImage
                  alt={user.name}
                  height={32}
                  src={user.avatar}
                  width={32}
                />
                <AvatarFallback className="rounded-lg">CN</AvatarFallback>
              </Avatar>
              <div className="grid flex-1 text-start text-sm leading-tight">
                <span className="truncate font-medium">{user.name}</span>
                <span className="truncate text-xs">{user.email}</span>
              </div>
              <ChevronsUpDownIcon
                aria-hidden="true"
                className="ms-auto size-4"
              />
            </SidebarMenuButton>
          </MenuTrigger>
          <MenuContent className="w-(--reference-width) min-w-56 rounded-lg">
            <MenuGroup>
              <MenuGroupLabel className="p-0 font-normal">
                <div className="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
                  <Avatar className="size-8 rounded-lg">
                    <AvatarImage
                      alt={user.name}
                      height={32}
                      src={user.avatar}
                      width={32}
                    />
                    <AvatarFallback className="rounded-lg">CN</AvatarFallback>
                  </Avatar>
                  <div className="grid flex-1 text-start text-sm leading-tight">
                    <span className="truncate font-medium">{user.name}</span>
                    <span className="truncate text-xs">{user.email}</span>
                  </div>
                </div>
              </MenuGroupLabel>
            </MenuGroup>
            <MenuSeparator />
            <MenuGroup>
              <MenuItem value="upgrade">
                <SparklesIcon aria-hidden="true" />
                Upgrade to Pro
              </MenuItem>
            </MenuGroup>
            <MenuSeparator />
            <MenuGroup>
              <MenuItem value="account">
                <BadgeCheckIcon aria-hidden="true" />
                Account
              </MenuItem>
              <MenuItem value="billing">
                <CreditCardIcon aria-hidden="true" />
                Billing
              </MenuItem>
              <MenuItem value="notifications">
                <BellIcon aria-hidden="true" />
                Notifications
              </MenuItem>
            </MenuGroup>
            <MenuSeparator />
            <MenuGroup>
              <MenuItem value="logout">
                <LogOutIcon aria-hidden="true" />
                Log out
              </MenuItem>
            </MenuGroup>
          </MenuContent>
        </Menu>
      </SidebarMenuItem>
    </SidebarMenu>
  );
};

const SidebarDemo = () => (
  <div className="h-[400px] w-full overflow-hidden rounded-xl border">
    <SidebarProvider
      className="h-full min-h-0"
      persist={false}
      shortcut={false}
    >
      <Sidebar className="h-full" collapsible="icon">
        <SidebarHeader>
          <TeamSwitcher teams={data.teams} />
        </SidebarHeader>
        <SidebarContent>
          <NavMain items={data.navMain} />
          <NavProjects projects={data.projects} />
        </SidebarContent>
        <SidebarFooter>
          <NavUser user={data.user} />
        </SidebarFooter>
        <SidebarRail />
      </Sidebar>
      <SidebarInset>
        <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
          <div className="flex items-center gap-2 px-4">
            <SidebarTrigger className="-ms-1" />
          </div>
        </header>
      </SidebarInset>
    </SidebarProvider>
  </div>
);

Accessibility

KeyDescription
Enter
Activate the focused control.
Space
Activate the focused control.
ArrowDown
Move down.
ArrowUp
Move up.
ArrowLeft
Move left.
ArrowRight
Move right.
Home
Go to the start.
End
Go to the end.

Demos

Rtl

Right-to-left sidebar with static Arabic labels.

API Reference