Siteplane UI

Sidebar

Build application navigation with desktop collapse and a mobile sheet.

Markdown
"use client";

import {
  ArrowUpRightIcon,
  BookMarkedIcon,
  BookOpenIcon,
  ChevronDownIcon,
  ChevronsUpDownIcon,
  CircleDotIcon,
  CircleHelpIcon,
  CopyIcon,
  FolderInputIcon,
  FolderPlusIcon,
  HeadsetIcon,
  HouseIcon,
  LayersIcon,
  LibraryIcon,
  LifeBuoyIcon,
  LogOutIcon,
  PencilIcon,
  PlusIcon,
  SettingsIcon,
  StarOffIcon,
  SunMoonIcon,
  Trash2Icon,
  UserIcon,
  UsersIcon,
} from "lucide-react";
import * as React from "react";

import { Avatar, AvatarFallback } from "@/siteplane/components/avatar";
import { Button } from "@/siteplane/components/button";
import { Card, CardPanel } from "@/siteplane/components/card";
import {
  Collapsible,
  CollapsiblePanel,
  CollapsibleTrigger,
} from "@/siteplane/components/collapsible";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from "@/siteplane/components/dropdown-menu";
import { Kbd } from "@/siteplane/components/kbd";
import {
  Tooltip,
  TooltipPopup,
  TooltipTrigger,
} from "@/siteplane/components/tooltip";
import {
  Sidebar,
  SidebarBrand,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuButton,
  SidebarMenuButtonDescription,
  SidebarMenuButtonText,
  SidebarMenuButtonTitle,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  sidebarMenuPopupWidthClassName,
  useSidebar,
} from "@/siteplane/components/sidebar";

type SidebarIconPath = string | readonly string[];

function SidebarIcon({
  iconPath,
  ...props
}: React.ComponentProps<"svg"> & {
  iconPath: SidebarIconPath;
}): React.ReactElement {
  const paths = typeof iconPath === "string" ? [iconPath] : iconPath;

  return (
    <svg aria-hidden="true" fill="currentColor" viewBox="0 0 24 24" {...props}>
      {paths.map((value) => (
        <path d={value} fill="currentColor" key={value} />
      ))}
    </svg>
  );
}

const sidebarIconPaths = {
  connectors:
    "M19.25 7C19.25 5.75736 18.2426 4.75 17 4.75C15.7574 4.75 14.75 5.75736 14.75 7C14.75 8.24264 15.7574 9.25 17 9.25C18.2426 9.25 19.25 8.24264 19.25 7ZM9.25 7C9.25 5.75736 8.24264 4.75 7 4.75C5.75736 4.75 4.75 5.75736 4.75 7C4.75 8.24264 5.75736 9.25 7 9.25C8.24264 9.25 9.25 8.24264 9.25 7ZM4.75 17C4.75 18.2426 5.75736 19.25 7 19.25C8.24264 19.25 9.25 18.2426 9.25 17C9.25 15.7574 8.24264 14.75 7 14.75C5.75736 14.75 4.75 15.7574 4.75 17ZM10.75 7C10.75 9.07107 9.07107 10.75 7 10.75C4.92893 10.75 3.25 9.07107 3.25 7C3.25 4.92893 4.92893 3.25 7 3.25C9.07107 3.25 10.75 4.92893 10.75 7ZM20.75 7C20.75 8.81421 19.4617 10.3273 17.75 10.6748V14C17.75 16.0711 16.0711 17.75 14 17.75H10.6748C10.3273 19.4617 8.81421 20.75 7 20.75C4.92893 20.75 3.25 19.0711 3.25 17C3.25 14.9289 4.92893 13.25 7 13.25C8.81421 13.25 10.3273 14.5383 10.6748 16.25H14C15.2426 16.25 16.25 15.2426 16.25 14V10.6748C14.5383 10.3273 13.25 8.81421 13.25 7C13.25 4.92893 14.9289 3.25 17 3.25C19.0711 3.25 20.75 4.92893 20.75 7Z",
  dashboard:
    "M18.2499 8.93869L13.3173 5.37717C12.5309 4.8092 11.469 4.8092 10.6825 5.37717L5.74992 8.93869V17.0002C5.74993 18.2429 6.75728 19.2502 7.99992 19.2502H15.9999C17.2426 19.2502 18.2499 18.2429 18.2499 17.0002V8.93869ZM14.9999 15.2502C15.4141 15.2502 15.7499 15.586 15.7499 16.0002C15.7499 16.4144 15.4141 16.7502 14.9999 16.7502H8.99992C8.58571 16.7502 8.24992 16.4144 8.24992 16.0002C8.24992 15.586 8.58571 15.2502 8.99992 15.2502H14.9999ZM19.7499 17.0002C19.7499 19.0713 18.071 20.7502 15.9999 20.7502H7.99992C5.92886 20.7502 4.24993 19.0713 4.24992 17.0002V10.0227L3.43938 10.6086C3.1036 10.8511 2.63405 10.7754 2.39153 10.4397C2.14902 10.1039 2.22471 9.63434 2.56047 9.39181L9.80461 4.16037C11.1151 3.21412 12.8847 3.21412 14.1952 4.16037L21.4394 9.39181C21.7751 9.63434 21.8508 10.1039 21.6083 10.4397C21.3658 10.7754 20.8963 10.8511 20.5605 10.6086L19.7499 10.0227V17.0002Z",
  ellipsis:
    "M6.75 12C6.75 12.9665 5.9665 13.75 5 13.75C4.0335 13.75 3.25 12.9665 3.25 12C3.25 11.0335 4.0335 10.25 5 10.25C5.9665 10.25 6.75 11.0335 6.75 12ZM13.75 12C13.75 12.9665 12.9665 13.75 12 13.75C11.0335 13.75 10.25 12.9665 10.25 12C10.25 11.0335 11.0335 10.25 12 10.25C12.9665 10.25 13.75 11.0335 13.75 12ZM20.75 12C20.75 12.9665 19.9665 13.75 19 13.75C18.0335 13.75 17.25 12.9665 17.25 12C17.25 11.0335 18.0335 10.25 19 10.25C19.9665 10.25 20.75 11.0335 20.75 12Z",
  inbox:
    "M20.25 12.75H15.6914C15.3847 14.4557 13.8935 15.75 12.0996 15.75H11.9004C10.1065 15.75 8.61526 14.4557 8.30859 12.75H3.75V16C3.75 17.2426 4.75736 18.25 6 18.25H18C19.2426 18.25 20.25 17.2426 20.25 16V12.75ZM7.88477 5.75C7.02064 5.75014 6.23237 6.24489 5.85742 7.02344L3.97266 10.9395C3.92435 11.0398 3.88631 11.1442 3.85352 11.25H9C9.41421 11.25 9.75 11.5858 9.75 12V12.0996C9.75 13.287 10.713 14.25 11.9004 14.25H12.0996C13.287 14.25 14.25 13.287 14.25 12.0996V12C14.25 11.5858 14.5858 11.25 15 11.25H20.1426C20.1105 11.1485 20.0747 11.0477 20.0283 10.9512L18.1416 7.02539C17.767 6.24579 16.9782 5.75001 16.1133 5.75H7.88477ZM21.75 16C21.75 18.0711 20.0711 19.75 18 19.75H6C3.92893 19.75 2.25 18.0711 2.25 16V11.915C2.25001 11.3518 2.37673 10.7956 2.62109 10.2881L4.50684 6.37305C5.13178 5.07536 6.44445 4.25014 7.88477 4.25H16.1133C17.5549 4.25001 18.8688 5.07664 19.4932 6.37598L21.3799 10.3018C21.6234 10.8085 21.7499 11.3636 21.75 11.9258V16Z",
  projects:
    "M9.25 15.9998C9.24992 15.3095 8.6903 14.7498 8 14.7498H6C5.30969 14.7498 4.75008 15.3095 4.75 15.9998V17.9998C4.75 18.6901 5.30964 19.2498 6 19.2498H8C8.69036 19.2498 9.25 18.6901 9.25 17.9998V15.9998ZM19.25 15.9998C19.2499 15.3095 18.6903 14.7498 18 14.7498H16C15.3097 14.7498 14.7501 15.3095 14.75 15.9998V17.9998C14.75 18.6901 15.3096 19.2498 16 19.2498H18C18.6904 19.2498 19.25 18.6901 19.25 17.9998V15.9998ZM15.0557 3.64139C16.1296 2.56745 17.8704 2.56745 18.9443 3.64139L20.3584 5.05546C21.4323 6.1294 21.4323 7.87019 20.3584 8.94413L18.9443 10.3592C17.8705 11.4327 16.1295 11.4327 15.0557 10.3592L13.6416 8.94413C12.5677 7.87019 12.5677 6.1294 13.6416 5.05546L15.0557 3.64139ZM9.25 5.99979C9.24992 5.30951 8.69031 4.74979 8 4.74979H6C5.30969 4.74979 4.75008 5.30951 4.75 5.99979V7.99979C4.75 8.69015 5.30964 9.24979 6 9.24979H8C8.69036 9.24979 9.25 8.69015 9.25 7.99979V5.99979ZM17.8838 4.70194C17.3956 4.21379 16.6044 4.21379 16.1162 4.70194L14.7021 6.116C14.214 6.60416 14.214 7.39543 14.7021 7.88358L16.1162 9.29764C16.6044 9.7858 17.3956 9.7858 17.8838 9.29764L19.2979 7.88358C19.786 7.39543 19.786 6.60416 19.2979 6.116L17.8838 4.70194ZM10.75 17.9998C10.75 19.5186 9.51878 20.7498 8 20.7498H6C4.48122 20.7498 3.25 19.5186 3.25 17.9998V15.9998C3.25008 14.4811 4.48127 13.2498 6 13.2498H8C9.51873 13.2498 10.7499 14.4811 10.75 15.9998V17.9998ZM20.75 17.9998C20.75 19.5186 19.5188 20.7498 18 20.7498H16C14.4812 20.7498 13.25 19.5186 13.25 17.9998V15.9998C13.2501 14.4811 14.4813 13.2498 16 13.2498H18C19.5187 13.2498 20.7499 14.4811 20.75 15.9998V17.9998ZM10.75 7.99979C10.75 9.51858 9.51878 10.7498 8 10.7498H6C4.48122 10.7498 3.25 9.51858 3.25 7.99979V5.99979C3.25008 4.48108 4.48127 3.24979 6 3.24979H8C9.51873 3.24979 10.7499 4.48108 10.75 5.99979V7.99979Z",
  resources:
    "M20.25 12C20.25 7.44365 16.5563 3.75 12 3.75C7.44365 3.75 3.75 7.44365 3.75 12C3.75 16.5563 7.44365 20.25 12 20.25C16.5563 20.25 20.25 16.5563 20.25 12ZM14.8525 8.26465C15.0984 8.21547 15.353 8.29241 15.5303 8.46973C15.7076 8.64704 15.7845 8.90157 15.7354 9.14746L14.7354 14.1475C14.6759 14.4441 14.4441 14.6759 14.1475 14.7354L9.14746 15.7354C8.90157 15.7845 8.64704 15.7076 8.46973 15.5303C8.29241 15.353 8.21547 15.0984 8.26465 14.8525L9.26465 9.85254L9.29492 9.74512C9.3831 9.50101 9.59286 9.31672 9.85254 9.26465L14.8525 8.26465ZM10.6367 10.6367L9.95605 14.0439L13.3623 13.3623L14.0439 9.95605L10.6367 10.6367ZM21.75 12C21.75 17.3848 17.3848 21.75 12 21.75C6.61522 21.75 2.25 17.3848 2.25 12C2.25 6.61522 6.61522 2.25 12 2.25C17.3848 2.25 21.75 6.61522 21.75 12Z",
  search:
    "M17.25 11a6.25 6.25 0 1 0-12.5 0 6.25 6.25 0 0 0 12.5 0m1.5 0c0 1.87-.663 3.585-1.766 4.924l4.046 4.046a.75.75 0 1 1-1.06 1.06l-4.046-4.046A7.75 7.75 0 1 1 18.75 11",
  shared:
    "M8.50032 12.25C8.91442 12.2501 9.25032 12.5859 9.25032 13C9.25032 13.4141 8.91442 13.7499 8.50032 13.75C6.62639 13.75 5.00405 14.8355 4.23079 16.415C4.07541 16.7328 4.13395 17.0174 4.34993 17.2666C4.58679 17.5398 5.01233 17.75 5.50032 17.75H7.50032C7.91442 17.7501 8.25032 18.0859 8.25032 18.5C8.25032 18.9141 7.91442 19.2499 7.50032 19.25H5.50032C4.6077 19.25 3.75725 18.8733 3.21614 18.249C2.65428 17.6008 2.43268 16.677 2.88411 15.7549C3.89957 13.6809 6.03215 12.25 8.50032 12.25ZM16.5003 12.25C19.17 12.2501 21.4473 13.9237 22.3431 16.2764C22.6501 17.0827 22.4285 17.8628 21.9417 18.4082C21.4702 18.9363 20.7541 19.2499 20.0003 19.25H13.0003C12.2464 19.25 11.5295 18.9365 11.0579 18.4082C10.5711 17.8628 10.3505 17.0828 10.6575 16.2764C11.5534 13.9237 13.8306 12.25 16.5003 12.25ZM16.5003 13.75C14.4734 13.75 12.7406 15.0203 12.0589 16.8105C11.9732 17.0362 12.0211 17.2344 12.1771 17.4092C12.3485 17.6012 12.6497 17.75 13.0003 17.75H20.0003C20.3507 17.7499 20.6512 17.601 20.8226 17.4092C20.9787 17.2343 21.0266 17.0363 20.9407 16.8105C20.2591 15.0204 18.5272 13.7501 16.5003 13.75ZM9.75032 8C9.75032 7.30972 9.19057 6.75013 8.50032 6.75C7.80996 6.75 7.25032 7.30964 7.25032 8C7.25032 8.69036 7.80996 9.25 8.50032 9.25C9.19057 9.24987 9.75032 8.69028 9.75032 8ZM18.2503 7.5C18.2503 6.53358 17.4667 5.75013 16.5003 5.75C15.5338 5.75 14.7503 6.5335 14.7503 7.5C14.7503 8.4665 15.5338 9.25 16.5003 9.25C17.4667 9.24987 18.2503 8.46642 18.2503 7.5ZM11.2503 8C11.2503 9.51871 10.019 10.7499 8.50032 10.75C6.98154 10.75 5.75032 9.51878 5.75032 8C5.75032 6.48122 6.98154 5.25 8.50032 5.25C10.019 5.25013 11.2503 6.48129 11.2503 8ZM19.7503 7.5C19.7503 9.29485 18.2951 10.7499 16.5003 10.75C14.7054 10.75 13.2503 9.29493 13.2503 7.5C13.2503 5.70507 14.7054 4.25 16.5003 4.25C18.2951 4.25013 19.7503 5.70515 19.7503 7.5Z",
  starred:
    "M12.0002 2.25C12.3031 2.25 12.5762 2.43225 12.6925 2.71191L14.8966 8.01172L20.6193 8.47168C20.9213 8.49589 21.179 8.69918 21.2726 8.9873C21.3661 9.27522 21.2777 9.591 21.048 9.78809L16.6877 13.5234L18.0197 19.1074C18.0898 19.402 17.9756 19.7097 17.7306 19.8877C17.4856 20.0656 17.158 20.0787 16.8996 19.9209L12.0002 16.9287L7.10074 19.9209C6.84228 20.0787 6.51473 20.0656 6.26968 19.8877C6.02466 19.7097 5.91046 19.402 5.98062 19.1074L7.31168 13.5234L2.9523 9.78809C2.72256 9.591 2.63418 9.27522 2.72769 8.9873C2.82131 8.69918 3.07903 8.49589 3.38101 8.47168L9.10269 8.01172L11.3078 2.71191L11.3586 2.6123C11.4929 2.38986 11.735 2.25 12.0002 2.25ZM10.3117 9.01172C10.2036 9.27112 9.95993 9.44813 9.67984 9.4707L5.298 9.82227L8.63589 12.6816C8.84949 12.8646 8.94334 13.1522 8.87808 13.4258L7.85757 17.7002L11.6095 15.4102L11.7023 15.3623C11.9241 15.2662 12.1809 15.282 12.3908 15.4102L16.1418 17.7002L15.1222 13.4258C15.057 13.1522 15.1508 12.8646 15.3644 12.6816L18.7013 9.82227L14.3205 9.4707C14.0404 9.44813 13.7967 9.27111 13.6886 9.01172L12.0002 4.95215L10.3117 9.01172Z",
  user: "M12.0006 12.25C15.523 12.2502 18.4949 14.5997 19.4381 17.8154C19.6746 18.6221 19.4321 19.3825 18.943 19.917C18.465 20.4392 17.7545 20.7499 17.0006 20.75H7.0006C6.24652 20.75 5.53531 20.4394 5.05724 19.917C4.56817 19.3826 4.32563 18.622 4.56212 17.8154C5.50533 14.5996 8.47805 12.25 12.0006 12.25ZM12.0006 13.75C9.16185 13.75 6.76251 15.6431 6.00158 18.2373C5.92731 18.4905 5.9923 18.717 6.16369 18.9043C6.34615 19.1037 6.65013 19.25 7.0006 19.25H17.0006C17.3509 19.2499 17.6541 19.1036 17.8365 18.9043C18.008 18.717 18.0729 18.4905 17.9986 18.2373C17.2377 15.6432 14.8392 13.7502 12.0006 13.75ZM14.2506 7C14.2506 5.75749 13.2431 4.75021 12.0006 4.75C10.758 4.75 9.7506 5.75736 9.7506 7C9.7506 8.24264 10.758 9.25 12.0006 9.25C13.2431 9.24979 14.2506 8.24251 14.2506 7ZM15.7506 7C15.7506 9.07094 14.0715 10.7498 12.0006 10.75C9.92953 10.75 8.2506 9.07107 8.2506 7C8.2506 4.92893 9.92953 3.25 12.0006 3.25C14.0715 3.25021 15.7506 4.92906 15.7506 7Z",
} as const;

type NavigationItem = {
  active?: boolean;
  href?: string;
  icon: SidebarIconPath;
  label: string;
  shortcut?: readonly string[];
};

const workspaces = [
  // A workspace with a logo shows it in its mark; one without shows its
  // initial.
  { icon: CircleDotIcon, initial: "A", name: "Acme", plan: "Pro plan" },
  { icon: null, initial: "L", name: "Acme Labs", plan: "Free plan" },
] as const;

const account = {
  avatar: "ME",
  email: "mara@example.com",
  name: "Mara Ellis",
};

const recentSpace = "Product roadmap";

const mainNavigation: NavigationItem[] = [
  {
    href: "#dashboard",
    icon: sidebarIconPaths.dashboard,
    label: "Dashboard",
  },
  {
    href: "#search",
    icon: sidebarIconPaths.search,
    label: "Search",
    shortcut: ["⌘", "K"],
  },
  {
    href: "#resources",
    icon: sidebarIconPaths.resources,
    label: "Resources",
  },
  {
    href: "#connectors",
    icon: sidebarIconPaths.connectors,
    label: "Connectors",
  },
];

const workspaceNavigation: NavigationItem[] = [
  {
    href: "#spaces",
    icon: sidebarIconPaths.projects,
    label: "All spaces",
  },
  { href: "#starred", icon: sidebarIconPaths.starred, label: "Starred" },
  {
    href: "#created-by-me",
    icon: sidebarIconPaths.user,
    label: "Created by me",
  },
  {
    href: "#shared-with-me",
    icon: sidebarIconPaths.shared,
    label: "Shared with me",
  },
];

type SidebarPatternLayout = "inset" | "push";

function ShellExample({
  layout,
}: {
  layout: SidebarPatternLayout;
}): React.ReactElement {
  const push = layout === "push";

  return (
    <div
      className="relative h-[640px] w-full overflow-hidden bg-[var(--sp-rw-bg-base)] [transform:translateZ(0)]"
      data-pattern="sidebar-workspace-example"
      data-sidebar-layout={layout}
    >
      <SidebarProvider className="h-full min-h-0!" layout={layout} defaultOpen>
        <Sidebar className="h-full!" collapsible="icon">
          <SidebarChrome layout={layout} />
        </Sidebar>

        <SidebarInset className="overflow-clip">
          <header className="flex h-12 shrink-0 items-center gap-2 px-4">
            <SidebarTrigger
              aria-label="Toggle sidebar"
              className={push ? undefined : "md:hidden"}
              size="icon-md"
            />
            <span className="text-sm text-[var(--sp-rw-fg-tertiary)]">
              Workspace
            </span>
            <span
              aria-hidden="true"
              className="text-[var(--sp-rw-fg-quaternary)]"
            >
              /
            </span>
            <span className="font-medium text-sm">Dashboard</span>
          </header>

          <div className="flex flex-1 flex-col gap-5 overflow-auto p-6">
            <div>
              <h2 className="font-medium text-xl">Welcome back</h2>
              <p className="mt-1 text-sm text-[var(--sp-rw-fg-tertiary)]">
                Continue building your Acme workspace.
              </p>
            </div>
            <Card className="h-28" />
            <div className="grid gap-4 sm:grid-cols-3">
              <Card className="h-32" />
              <Card className="h-32" />
              <Card className="h-32" />
            </div>
            <Card className="h-40" />
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}

export default function Example() {
  return <ShellExample layout="inset" />;
}

type SidebarHeaderSlot = React.ComponentType<
  React.ComponentProps<typeof SidebarHeader>
>;

function SidebarChrome({
  Header = SidebarHeader,
  layout = "inset",
}: {
  Header?: SidebarHeaderSlot;
  layout?: SidebarPatternLayout;
} = {}): React.ReactElement {
  const [activeItem, setActiveItem] = React.useState("Dashboard");

  return (
    <>
      {/* The first row holds the logo, and the collapse button in the
          inset layout; it keeps the top bar's height beside it. */}
      <Header className="flex-row items-center justify-between gap-1">
        {/* On the collapsed rail the logo's mark expands the sidebar, at the
            height of the collapse button; the workspace below keeps its menu. */}
        <SidebarMenu className="min-w-0 flex-1">
          <SidebarMenuItem expandTrigger>
            <SidebarBrand render={<a href="#home" />}>
              <NorthlightMark />
              Northlight
            </SidebarBrand>
          </SidebarMenuItem>
        </SidebarMenu>
        {layout === "inset" ? <InsetSidebarToggle /> : null}
      </Header>

      {/* The workspace switcher is a row of its own outside SidebarContent,
          so it stays in place while the navigation scrolls. */}
      <SidebarHeader>
        <SidebarMenu>
          <WorkspaceSwitcher />
        </SidebarMenu>
      </SidebarHeader>

      <SidebarContent>
        <SidebarGroup>
          <SidebarNavigation
            activeItem={activeItem}
            items={mainNavigation}
            onActiveItemChange={setActiveItem}
          />
        </SidebarGroup>

        <SidebarGroup>
          <SidebarGroupLabel>Workspace</SidebarGroupLabel>
          <WorkspaceNavigation
            activeItem={activeItem}
            onActiveItemChange={setActiveItem}
          />
        </SidebarGroup>

        <RecentSpaces
          activeItem={activeItem}
          onActiveItemChange={setActiveItem}
        />
      </SidebarContent>

      <SidebarFooter>
        <UpgradeCard />
        <FooterNavigation />
      </SidebarFooter>

      <SidebarRail resizable />
    </>
  );
}

function InsetSidebarToggle(): React.ReactElement {
  return (
    <SidebarTrigger
      aria-label="Collapse sidebar"
      className="group-data-[collapsible=icon]:hidden"
      size="icon-md"
    />
  );
}

/** A neutral placeholder logo: a four-point star in a rounded square. */
function NorthlightMark(): React.ReactElement {
  return (
    <svg aria-hidden="true" className="size-5" viewBox="0 0 20 20">
      <rect
        className="fill-[var(--sp-rw-bg-inverse)]"
        height="20"
        rx="6"
        width="20"
      />
      <path
        className="fill-[var(--sp-rw-fg-inverse)]"
        d="M10 4.5 11.4 8.6 15.5 10 11.4 11.4 10 15.5 8.6 11.4 4.5 10 8.6 8.6Z"
      />
    </svg>
  );
}

function SidebarNavigation({
  activeItem,
  items,
  onActiveItemChange,
}: {
  activeItem: string;
  items: NavigationItem[];
  onActiveItemChange: (label: string) => void;
}): React.ReactElement {
  return (
    <SidebarMenu className="gap-px">
      {items.map((item) => {
        return (
          <SidebarMenuItem key={item.label}>
            <SidebarMenuButton
              active={activeItem === item.label}
              onClick={() => onActiveItemChange(item.label)}
              render={item.href ? <a href={item.href} /> : undefined}
              tooltip={item.label}
              type={item.href ? undefined : "button"}
            >
              <SidebarIcon className="size-5 shrink-0" iconPath={item.icon} />
              <span>{item.label}</span>
              {item.shortcut ? (
                <span className="ms-auto flex gap-px">
                  {item.shortcut.map((key) => (
                    <Kbd key={key}>{key}</Kbd>
                  ))}
                </span>
              ) : null}
            </SidebarMenuButton>
          </SidebarMenuItem>
        );
      })}
    </SidebarMenu>
  );
}

function WorkspaceNavigation({
  activeItem,
  onActiveItemChange,
}: {
  activeItem: string;
  onActiveItemChange: (label: string) => void;
}): React.ReactElement {
  return (
    <SidebarMenu className="gap-px">
      {workspaceNavigation.map((item, index) => (
        <WorkspaceFolderRow
          active={activeItem === item.label}
          hasActions={index === 0}
          item={item}
          key={item.label}
          onSelect={() => onActiveItemChange(item.label)}
        />
      ))}
      <LibraryNavigation
        activeItem={activeItem}
        onActiveItemChange={onActiveItemChange}
      />
    </SidebarMenu>
  );
}

function LibraryNavigation({
  activeItem,
  onActiveItemChange,
}: {
  activeItem: string;
  onActiveItemChange: (label: string) => void;
}): React.ReactElement {
  const { isMobile, state, setOpen } = useSidebar();

  return (
    <Collapsible
      onOpenChange={(open) => {
        if (open && !isMobile && state === "collapsed") setOpen(true);
      }}
      render={<SidebarMenuItem />}
    >
      <CollapsibleTrigger render={<SidebarMenuButton tooltip="Library" />}>
        <LibraryIcon className="size-5" />
        <span>Library</span>
        <span className="ms-1 flex shrink-0 items-center group-data-[collapsible=icon]:hidden">
          <ChevronDownIcon
            className="m-0! size-4"
            data-slot="collapsible-chevron"
          />
        </span>
      </CollapsibleTrigger>
      <CollapsiblePanel>
        <SidebarMenuSub>
          {[
            { label: "Documents", href: "#documents" },
            { label: "Templates", href: "#templates" },
            { label: "Media", href: "#media" },
          ].map((item) => (
            <SidebarMenuSubItem key={item.label}>
              <SidebarMenuSubButton
                active={activeItem === item.label}
                onClick={() => onActiveItemChange(item.label)}
                render={<a href={item.href} />}
              >
                {item.label}
              </SidebarMenuSubButton>
            </SidebarMenuSubItem>
          ))}
        </SidebarMenuSub>
      </CollapsiblePanel>
    </Collapsible>
  );
}

function WorkspaceFolderRow({
  active,
  hasActions,
  item,
  onSelect,
}: {
  active: boolean;
  hasActions: boolean;
  item: NavigationItem;
  onSelect: () => void;
}): React.ReactElement {
  return (
    <SidebarMenuItem className="group/folder-row">
      <SidebarMenuButton
        active={active}
        onClick={onSelect}
        render={<a href={item.href} />}
        tooltip={item.label}
      >
        <SidebarIcon
          aria-hidden="true"
          className="size-5"
          iconPath={item.icon}
        />
        <span>{item.label}</span>
      </SidebarMenuButton>

      {hasActions ? (
        <DropdownMenu>
          <DropdownMenuTrigger
            render={
              <SidebarMenuAction
                aria-label={`Options for ${item.label}`}
                showOnHover
                type="button"
              />
            }
          >
            <SidebarIcon
              className="size-5 shrink-0 opacity-100"
              iconPath={sidebarIconPaths.ellipsis}
            />
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end" className="w-[180px]" sideOffset={4}>
            <DropdownMenuItem>
              <PlusIcon />
              Create space
            </DropdownMenuItem>
            <DropdownMenuItem>
              <FolderPlusIcon />
              Create folder
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      ) : null}
    </SidebarMenuItem>
  );
}

// The switcher is a row of its own under the logo: the workspace's mark, its
// name over its plan and the up-down chevron. Two lines fill the 48 px row; on
// the collapsed rail it is the 36 px square with a 20 px mark, and the mark
// still opens the workspace menu there; the logo above expands the rail.
function WorkspaceSwitcher(): React.ReactElement {
  const { isMobile, state } = useSidebar();
  const collapsed = state === "collapsed" && !isMobile;
  const [activeName, setActiveName] = React.useState<string>(
    workspaces[0].name,
  );
  const active =
    workspaces.find((workspace) => workspace.name === activeName) ??
    workspaces[0];

  return (
    <SidebarMenuItem>
      <DropdownMenu>
        <DropdownMenuTrigger
          render={
            <SidebarMenuButton
              aria-label={`Open workspace switcher for ${active.name}`}
              size={collapsed ? "lg" : "xl"}
              type="button"
              variant="workspace"
            />
          }
        >
          <Avatar
            aria-hidden="true"
            className={collapsed ? "size-5 rounded-md" : "size-9 rounded-md"}
          >
            <AvatarFallback>
              {active.icon ? (
                <active.icon className="size-3/5" />
              ) : (
                active.initial
              )}
            </AvatarFallback>
          </Avatar>
          <SidebarMenuButtonText>
            <SidebarMenuButtonTitle>{active.name}</SidebarMenuButtonTitle>
            <SidebarMenuButtonDescription>
              {active.plan}
            </SidebarMenuButtonDescription>
          </SidebarMenuButtonText>
          <ChevronsUpDownIcon className="size-4 text-[var(--sp-rw-fg-tertiary)]" />
        </DropdownMenuTrigger>
        <DropdownMenuContent
          align="start"
          className={collapsed ? sidebarMenuPopupWidthClassName : "w-[280px]"}
          side="bottom"
          sideOffset={4}
        >
          <DropdownMenuGroup>
            <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
            <DropdownMenuRadioGroup
              onValueChange={(name) => setActiveName(String(name))}
              value={active.name}
            >
              {workspaces.map((workspace) => (
                <DropdownMenuRadioItem
                  closeOnClick
                  indicatorPosition="end"
                  key={workspace.name}
                  value={workspace.name}
                >
                  <Avatar aria-hidden="true" className="size-6 rounded-md">
                    <AvatarFallback>
                      {workspace.icon ? (
                        <workspace.icon className="size-3/5" />
                      ) : (
                        workspace.initial
                      )}
                    </AvatarFallback>
                  </Avatar>
                  {workspace.name}
                </DropdownMenuRadioItem>
              ))}
            </DropdownMenuRadioGroup>
          </DropdownMenuGroup>
          <DropdownMenuSeparator />
          <DropdownMenuItem>
            <PlusIcon />
            <span>New workspace</span>
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </SidebarMenuItem>
  );
}

function RecentSpaces({
  activeItem,
  onActiveItemChange,
}: {
  activeItem: string;
  onActiveItemChange: (label: string) => void;
}): React.ReactElement {
  return (
    <SidebarGroup className="group-data-[collapsible=icon]:hidden">
      <SidebarGroupLabel>Recently used</SidebarGroupLabel>
      <SidebarMenu>
        <SidebarMenuItem className="group/project-row">
          <SidebarMenuButton
            active={activeItem === recentSpace}
            onClick={() => onActiveItemChange(recentSpace)}
            render={<a href="#recent-space" />}
            tooltip={recentSpace}
          >
            <span>{recentSpace}</span>
          </SidebarMenuButton>
          <DropdownMenu>
            <DropdownMenuTrigger
              render={
                <SidebarMenuAction
                  aria-label={`More options for ${recentSpace}`}
                  showOnHover
                  type="button"
                />
              }
            >
              <SidebarIcon
                className="size-5 shrink-0 opacity-100"
                iconPath={sidebarIconPaths.ellipsis}
              />
            </DropdownMenuTrigger>
            <DropdownMenuContent
              align="end"
              className="w-[198px]"
              side="right"
              sideOffset={4}
            >
              <DropdownMenuItem>
                <StarOffIcon />
                Unstar
              </DropdownMenuItem>
              <DropdownMenuItem>
                <FolderInputIcon />
                Move to folder
              </DropdownMenuItem>
              <DropdownMenuItem>
                <CopyIcon />
                Duplicate
              </DropdownMenuItem>
              <DropdownMenuItem>
                <PencilIcon />
                Rename
              </DropdownMenuItem>
              <DropdownMenuItem>
                <SettingsIcon />
                Settings
              </DropdownMenuItem>
              <DropdownMenuSeparator />
              <DropdownMenuItem variant="destructive">
                <Trash2Icon />
                Delete
              </DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        </SidebarMenuItem>
      </SidebarMenu>
    </SidebarGroup>
  );
}

function UpgradeCard(): React.ReactElement {
  return (
    <Card className="group-data-[collapsible=icon]:hidden">
      <CardPanel className="flex items-center justify-between gap-3">
        <span className="flex min-w-0 flex-col">
          <span className="truncate font-medium text-sm leading-5">
            Make room for more
          </span>
          <span className="truncate text-xs leading-4 text-[var(--sp-rw-fg-tertiary)]">
            Explore workspace plans
          </span>
        </span>
        <Button
          aria-label="Explore workspace plans"
          render={<a href="#billing" />}
          shape="pill"
          size="icon-md"
          variant="muted-accent"
        >
          <ArrowUpRightIcon />
        </Button>
      </CardPanel>
    </Card>
  );
}

function FooterNavigation(): React.ReactElement {
  const [theme, setTheme] = React.useState("system");

  // On the collapsed rail the Messages link stacks above the avatar in the
  // rail's 36 px square: the avatar keeps its place at the bottom, and both
  // stand on the rail's axis from the first frame of the collapse.
  return (
    <div className="flex items-center justify-between group-data-[collapsible=icon]:w-(--sp-rw-control-lg) group-data-[collapsible=icon]:flex-col-reverse group-data-[collapsible=icon]:gap-2">
      <DropdownMenu>
        <Tooltip>
          <TooltipTrigger
            delay={0}
            render={
              <DropdownMenuTrigger
                render={
                  <Button
                    aria-label="Profile"
                    profile="avatar"
                    size="icon-md"
                    type="button"
                    variant="ghost"
                  />
                }
              />
            }
          >
            <Avatar>
              <AvatarFallback>{account.avatar}</AvatarFallback>
            </Avatar>
          </TooltipTrigger>
          <TooltipPopup side="right">Profile</TooltipPopup>
        </Tooltip>
        <DropdownMenuContent
          align="start"
          className="w-[220px]"
          side="top"
          sideOffset={4}
        >
          <DropdownMenuGroup>
            <DropdownMenuLabel className="flex items-center gap-2">
              <Avatar aria-hidden="true">
                <AvatarFallback>{account.avatar}</AvatarFallback>
              </Avatar>
              <span className="grid min-w-0">
                <span className="truncate font-medium text-sm text-[var(--sp-rw-fg-primary)]">
                  {account.name}
                </span>
                <span className="truncate font-normal text-xs text-[var(--sp-rw-fg-tertiary)]">
                  {account.email}
                </span>
              </span>
            </DropdownMenuLabel>
          </DropdownMenuGroup>
          <DropdownMenuSeparator />
          <DropdownMenuItem>
            <UserIcon />
            <span>Profile</span>
          </DropdownMenuItem>
          <DropdownMenuItem>
            <SettingsIcon />
            <span>Settings</span>
            <DropdownMenuShortcut>⌘,</DropdownMenuShortcut>
          </DropdownMenuItem>
          <DropdownMenuSub>
            <DropdownMenuSubTrigger>
              <SunMoonIcon />
              <span>Appearance</span>
            </DropdownMenuSubTrigger>
            <DropdownMenuSubContent className="w-[180px]">
              <DropdownMenuRadioGroup onValueChange={setTheme} value={theme}>
                <DropdownMenuRadioItem value="system">
                  System
                </DropdownMenuRadioItem>
                <DropdownMenuRadioItem value="light">
                  Light
                </DropdownMenuRadioItem>
                <DropdownMenuRadioItem value="dark">Dark</DropdownMenuRadioItem>
              </DropdownMenuRadioGroup>
            </DropdownMenuSubContent>
          </DropdownMenuSub>
          <DropdownMenuSeparator />
          <DropdownMenuSub>
            <DropdownMenuSubTrigger>
              <CircleHelpIcon />
              <span>Support</span>
            </DropdownMenuSubTrigger>
            <DropdownMenuSubContent className="w-[180px]">
              <DropdownMenuItem>
                <LifeBuoyIcon />
                Help center
              </DropdownMenuItem>
              <DropdownMenuItem>
                <HeadsetIcon />
                Contact support
              </DropdownMenuItem>
            </DropdownMenuSubContent>
          </DropdownMenuSub>
          <DropdownMenuSub>
            <DropdownMenuSubTrigger>
              <BookOpenIcon />
              <span>Documentation</span>
            </DropdownMenuSubTrigger>
            <DropdownMenuSubContent className="w-[180px]">
              <DropdownMenuItem>
                <BookMarkedIcon />
                Guides
              </DropdownMenuItem>
              <DropdownMenuItem>
                <LayersIcon />
                Changelog
              </DropdownMenuItem>
            </DropdownMenuSubContent>
          </DropdownMenuSub>
          <DropdownMenuItem>
            <UsersIcon />
            <span>Community</span>
            <ArrowUpRightIcon />
          </DropdownMenuItem>
          <DropdownMenuItem>
            <HouseIcon />
            <span>Homepage</span>
          </DropdownMenuItem>
          <DropdownMenuSeparator />
          <DropdownMenuItem>
            <LogOutIcon />
            <span>Sign out</span>
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>

      <Tooltip>
        <TooltipTrigger
          delay={0}
          render={
            <Button
              aria-label="Messages"
              profile="inline"
              profileTone="muted"
              render={<a href="#messages" />}
              size="xl"
              variant="text"
            />
          }
        >
          <SidebarIcon
            className="shrink-0 opacity-100"
            iconPath={sidebarIconPaths.inbox}
          />
        </TooltipTrigger>
        <TooltipPopup side="right">Messages</TooltipPopup>
      </Tooltip>
    </div>
  );
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/sidebar

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

pnpm dlx shadcn@4.18.0 add @siteplane/avatar @siteplane/button @siteplane/card @siteplane/collapsible @siteplane/dropdown-menu @siteplane/kbd @siteplane/tooltip

Or install the complete inset example with its dependencies; use @siteplane/sidebar-demo for the push layout:

pnpm dlx shadcn@4.18.0 add @siteplane/sidebar-inset-example

Open the full-width interactive example. Set layout="inset" on SidebarProvider for a framed content surface. The first row holds the logo and the collapse button, and the workspace switcher follows in a row of its own; when the sidebar is collapsed, hover or focus the logo's mark to reveal the expand control. Below 768 px, the header toggle opens the navigation as a sheet.

Usage

import {
  SidebarProvider,
  Sidebar,
  SidebarContent,
  SidebarInset,
  SidebarTrigger,
} from "@/siteplane/components/sidebar";

<SidebarProvider>
  <Sidebar>
    <SidebarContent>Navigation</SidebarContent>
  </Sidebar>
  <SidebarInset>
    <SidebarTrigger />
    Content
  </SidebarInset>
</SidebarProvider>;

Composition

SidebarProvider
  Sidebar
    SidebarHeader → SidebarBrand + SidebarTrigger
    SidebarHeader → SidebarMenu → SidebarMenuItem (workspace switcher)
    SidebarContent → SidebarGroup → SidebarGroupLabel + SidebarGroupContent
      SidebarMenu → SidebarMenuItem → SidebarMenuButton + SidebarMenuAction + SidebarMenuBadge
        SidebarMenuButton → SidebarMenuButtonText → SidebarMenuButtonTitle + SidebarMenuButtonDescription
        SidebarMenuSub → SidebarMenuSubItem → SidebarMenuSubButton
    SidebarFooter
    SidebarRail
  SidebarInset → SidebarTrigger + application content

The complete examples combine these parts:

FeaturePublic composition
Application logoSidebarBrand in a SidebarHeader
Named navigation groupsSidebarGroup + SidebarGroupLabel + SidebarMenu
Expandable nested linksCollapsible + SidebarMenuButton + SidebarMenuSub
Workspace and account dropdownsDropdownMenu with a sidebar button as its trigger
Name over a detail lineSidebarMenuButtonText in a size="xl" menu button
Per-item actionsSidebarMenuAction + DropdownMenu
Desktop collapse and resizeSidebarTrigger + SidebarRail resizable
Mobile navigationSidebarProvider + Sidebar (built-in Sheet)

Push layout

Set layout="push" on SidebarProvider for one unframed content surface from the sidebar to the edge. The header button toggles the sidebar; expanding it pushes the header and content aside instead of covering them. SidebarInset moves its content at the sidebar's pace, so its children keep their own padding and need no offset or motion code. Put padding on those children rather than on SidebarInset: a padding utility on SidebarInset replaces the offset.

"use client";

import {
  ArrowUpRightIcon,
  BookMarkedIcon,
  BookOpenIcon,
  ChevronDownIcon,
  ChevronsUpDownIcon,
  CircleDotIcon,
  CircleHelpIcon,
  CopyIcon,
  FolderInputIcon,
  FolderPlusIcon,
  HeadsetIcon,
  HouseIcon,
  LayersIcon,
  LibraryIcon,
  LifeBuoyIcon,
  LogOutIcon,
  PencilIcon,
  PlusIcon,
  SettingsIcon,
  StarOffIcon,
  SunMoonIcon,
  Trash2Icon,
  UserIcon,
  UsersIcon,
} from "lucide-react";
import * as React from "react";

import { Avatar, AvatarFallback } from "@/siteplane/components/avatar";
import { Button } from "@/siteplane/components/button";
import { Card, CardPanel } from "@/siteplane/components/card";
import {
  Collapsible,
  CollapsiblePanel,
  CollapsibleTrigger,
} from "@/siteplane/components/collapsible";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from "@/siteplane/components/dropdown-menu";
import { Kbd } from "@/siteplane/components/kbd";
import {
  Tooltip,
  TooltipPopup,
  TooltipTrigger,
} from "@/siteplane/components/tooltip";
import {
  Sidebar,
  SidebarBrand,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuButton,
  SidebarMenuButtonDescription,
  SidebarMenuButtonText,
  SidebarMenuButtonTitle,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  sidebarMenuPopupWidthClassName,
  useSidebar,
} from "@/siteplane/components/sidebar";

type SidebarIconPath = string | readonly string[];

function SidebarIcon({
  iconPath,
  ...props
}: React.ComponentProps<"svg"> & {
  iconPath: SidebarIconPath;
}): React.ReactElement {
  const paths = typeof iconPath === "string" ? [iconPath] : iconPath;

  return (
    <svg aria-hidden="true" fill="currentColor" viewBox="0 0 24 24" {...props}>
      {paths.map((value) => (
        <path d={value} fill="currentColor" key={value} />
      ))}
    </svg>
  );
}

const sidebarIconPaths = {
  connectors:
    "M19.25 7C19.25 5.75736 18.2426 4.75 17 4.75C15.7574 4.75 14.75 5.75736 14.75 7C14.75 8.24264 15.7574 9.25 17 9.25C18.2426 9.25 19.25 8.24264 19.25 7ZM9.25 7C9.25 5.75736 8.24264 4.75 7 4.75C5.75736 4.75 4.75 5.75736 4.75 7C4.75 8.24264 5.75736 9.25 7 9.25C8.24264 9.25 9.25 8.24264 9.25 7ZM4.75 17C4.75 18.2426 5.75736 19.25 7 19.25C8.24264 19.25 9.25 18.2426 9.25 17C9.25 15.7574 8.24264 14.75 7 14.75C5.75736 14.75 4.75 15.7574 4.75 17ZM10.75 7C10.75 9.07107 9.07107 10.75 7 10.75C4.92893 10.75 3.25 9.07107 3.25 7C3.25 4.92893 4.92893 3.25 7 3.25C9.07107 3.25 10.75 4.92893 10.75 7ZM20.75 7C20.75 8.81421 19.4617 10.3273 17.75 10.6748V14C17.75 16.0711 16.0711 17.75 14 17.75H10.6748C10.3273 19.4617 8.81421 20.75 7 20.75C4.92893 20.75 3.25 19.0711 3.25 17C3.25 14.9289 4.92893 13.25 7 13.25C8.81421 13.25 10.3273 14.5383 10.6748 16.25H14C15.2426 16.25 16.25 15.2426 16.25 14V10.6748C14.5383 10.3273 13.25 8.81421 13.25 7C13.25 4.92893 14.9289 3.25 17 3.25C19.0711 3.25 20.75 4.92893 20.75 7Z",
  dashboard:
    "M18.2499 8.93869L13.3173 5.37717C12.5309 4.8092 11.469 4.8092 10.6825 5.37717L5.74992 8.93869V17.0002C5.74993 18.2429 6.75728 19.2502 7.99992 19.2502H15.9999C17.2426 19.2502 18.2499 18.2429 18.2499 17.0002V8.93869ZM14.9999 15.2502C15.4141 15.2502 15.7499 15.586 15.7499 16.0002C15.7499 16.4144 15.4141 16.7502 14.9999 16.7502H8.99992C8.58571 16.7502 8.24992 16.4144 8.24992 16.0002C8.24992 15.586 8.58571 15.2502 8.99992 15.2502H14.9999ZM19.7499 17.0002C19.7499 19.0713 18.071 20.7502 15.9999 20.7502H7.99992C5.92886 20.7502 4.24993 19.0713 4.24992 17.0002V10.0227L3.43938 10.6086C3.1036 10.8511 2.63405 10.7754 2.39153 10.4397C2.14902 10.1039 2.22471 9.63434 2.56047 9.39181L9.80461 4.16037C11.1151 3.21412 12.8847 3.21412 14.1952 4.16037L21.4394 9.39181C21.7751 9.63434 21.8508 10.1039 21.6083 10.4397C21.3658 10.7754 20.8963 10.8511 20.5605 10.6086L19.7499 10.0227V17.0002Z",
  ellipsis:
    "M6.75 12C6.75 12.9665 5.9665 13.75 5 13.75C4.0335 13.75 3.25 12.9665 3.25 12C3.25 11.0335 4.0335 10.25 5 10.25C5.9665 10.25 6.75 11.0335 6.75 12ZM13.75 12C13.75 12.9665 12.9665 13.75 12 13.75C11.0335 13.75 10.25 12.9665 10.25 12C10.25 11.0335 11.0335 10.25 12 10.25C12.9665 10.25 13.75 11.0335 13.75 12ZM20.75 12C20.75 12.9665 19.9665 13.75 19 13.75C18.0335 13.75 17.25 12.9665 17.25 12C17.25 11.0335 18.0335 10.25 19 10.25C19.9665 10.25 20.75 11.0335 20.75 12Z",
  inbox:
    "M20.25 12.75H15.6914C15.3847 14.4557 13.8935 15.75 12.0996 15.75H11.9004C10.1065 15.75 8.61526 14.4557 8.30859 12.75H3.75V16C3.75 17.2426 4.75736 18.25 6 18.25H18C19.2426 18.25 20.25 17.2426 20.25 16V12.75ZM7.88477 5.75C7.02064 5.75014 6.23237 6.24489 5.85742 7.02344L3.97266 10.9395C3.92435 11.0398 3.88631 11.1442 3.85352 11.25H9C9.41421 11.25 9.75 11.5858 9.75 12V12.0996C9.75 13.287 10.713 14.25 11.9004 14.25H12.0996C13.287 14.25 14.25 13.287 14.25 12.0996V12C14.25 11.5858 14.5858 11.25 15 11.25H20.1426C20.1105 11.1485 20.0747 11.0477 20.0283 10.9512L18.1416 7.02539C17.767 6.24579 16.9782 5.75001 16.1133 5.75H7.88477ZM21.75 16C21.75 18.0711 20.0711 19.75 18 19.75H6C3.92893 19.75 2.25 18.0711 2.25 16V11.915C2.25001 11.3518 2.37673 10.7956 2.62109 10.2881L4.50684 6.37305C5.13178 5.07536 6.44445 4.25014 7.88477 4.25H16.1133C17.5549 4.25001 18.8688 5.07664 19.4932 6.37598L21.3799 10.3018C21.6234 10.8085 21.7499 11.3636 21.75 11.9258V16Z",
  projects:
    "M9.25 15.9998C9.24992 15.3095 8.6903 14.7498 8 14.7498H6C5.30969 14.7498 4.75008 15.3095 4.75 15.9998V17.9998C4.75 18.6901 5.30964 19.2498 6 19.2498H8C8.69036 19.2498 9.25 18.6901 9.25 17.9998V15.9998ZM19.25 15.9998C19.2499 15.3095 18.6903 14.7498 18 14.7498H16C15.3097 14.7498 14.7501 15.3095 14.75 15.9998V17.9998C14.75 18.6901 15.3096 19.2498 16 19.2498H18C18.6904 19.2498 19.25 18.6901 19.25 17.9998V15.9998ZM15.0557 3.64139C16.1296 2.56745 17.8704 2.56745 18.9443 3.64139L20.3584 5.05546C21.4323 6.1294 21.4323 7.87019 20.3584 8.94413L18.9443 10.3592C17.8705 11.4327 16.1295 11.4327 15.0557 10.3592L13.6416 8.94413C12.5677 7.87019 12.5677 6.1294 13.6416 5.05546L15.0557 3.64139ZM9.25 5.99979C9.24992 5.30951 8.69031 4.74979 8 4.74979H6C5.30969 4.74979 4.75008 5.30951 4.75 5.99979V7.99979C4.75 8.69015 5.30964 9.24979 6 9.24979H8C8.69036 9.24979 9.25 8.69015 9.25 7.99979V5.99979ZM17.8838 4.70194C17.3956 4.21379 16.6044 4.21379 16.1162 4.70194L14.7021 6.116C14.214 6.60416 14.214 7.39543 14.7021 7.88358L16.1162 9.29764C16.6044 9.7858 17.3956 9.7858 17.8838 9.29764L19.2979 7.88358C19.786 7.39543 19.786 6.60416 19.2979 6.116L17.8838 4.70194ZM10.75 17.9998C10.75 19.5186 9.51878 20.7498 8 20.7498H6C4.48122 20.7498 3.25 19.5186 3.25 17.9998V15.9998C3.25008 14.4811 4.48127 13.2498 6 13.2498H8C9.51873 13.2498 10.7499 14.4811 10.75 15.9998V17.9998ZM20.75 17.9998C20.75 19.5186 19.5188 20.7498 18 20.7498H16C14.4812 20.7498 13.25 19.5186 13.25 17.9998V15.9998C13.2501 14.4811 14.4813 13.2498 16 13.2498H18C19.5187 13.2498 20.7499 14.4811 20.75 15.9998V17.9998ZM10.75 7.99979C10.75 9.51858 9.51878 10.7498 8 10.7498H6C4.48122 10.7498 3.25 9.51858 3.25 7.99979V5.99979C3.25008 4.48108 4.48127 3.24979 6 3.24979H8C9.51873 3.24979 10.7499 4.48108 10.75 5.99979V7.99979Z",
  resources:
    "M20.25 12C20.25 7.44365 16.5563 3.75 12 3.75C7.44365 3.75 3.75 7.44365 3.75 12C3.75 16.5563 7.44365 20.25 12 20.25C16.5563 20.25 20.25 16.5563 20.25 12ZM14.8525 8.26465C15.0984 8.21547 15.353 8.29241 15.5303 8.46973C15.7076 8.64704 15.7845 8.90157 15.7354 9.14746L14.7354 14.1475C14.6759 14.4441 14.4441 14.6759 14.1475 14.7354L9.14746 15.7354C8.90157 15.7845 8.64704 15.7076 8.46973 15.5303C8.29241 15.353 8.21547 15.0984 8.26465 14.8525L9.26465 9.85254L9.29492 9.74512C9.3831 9.50101 9.59286 9.31672 9.85254 9.26465L14.8525 8.26465ZM10.6367 10.6367L9.95605 14.0439L13.3623 13.3623L14.0439 9.95605L10.6367 10.6367ZM21.75 12C21.75 17.3848 17.3848 21.75 12 21.75C6.61522 21.75 2.25 17.3848 2.25 12C2.25 6.61522 6.61522 2.25 12 2.25C17.3848 2.25 21.75 6.61522 21.75 12Z",
  search:
    "M17.25 11a6.25 6.25 0 1 0-12.5 0 6.25 6.25 0 0 0 12.5 0m1.5 0c0 1.87-.663 3.585-1.766 4.924l4.046 4.046a.75.75 0 1 1-1.06 1.06l-4.046-4.046A7.75 7.75 0 1 1 18.75 11",
  shared:
    "M8.50032 12.25C8.91442 12.2501 9.25032 12.5859 9.25032 13C9.25032 13.4141 8.91442 13.7499 8.50032 13.75C6.62639 13.75 5.00405 14.8355 4.23079 16.415C4.07541 16.7328 4.13395 17.0174 4.34993 17.2666C4.58679 17.5398 5.01233 17.75 5.50032 17.75H7.50032C7.91442 17.7501 8.25032 18.0859 8.25032 18.5C8.25032 18.9141 7.91442 19.2499 7.50032 19.25H5.50032C4.6077 19.25 3.75725 18.8733 3.21614 18.249C2.65428 17.6008 2.43268 16.677 2.88411 15.7549C3.89957 13.6809 6.03215 12.25 8.50032 12.25ZM16.5003 12.25C19.17 12.2501 21.4473 13.9237 22.3431 16.2764C22.6501 17.0827 22.4285 17.8628 21.9417 18.4082C21.4702 18.9363 20.7541 19.2499 20.0003 19.25H13.0003C12.2464 19.25 11.5295 18.9365 11.0579 18.4082C10.5711 17.8628 10.3505 17.0828 10.6575 16.2764C11.5534 13.9237 13.8306 12.25 16.5003 12.25ZM16.5003 13.75C14.4734 13.75 12.7406 15.0203 12.0589 16.8105C11.9732 17.0362 12.0211 17.2344 12.1771 17.4092C12.3485 17.6012 12.6497 17.75 13.0003 17.75H20.0003C20.3507 17.7499 20.6512 17.601 20.8226 17.4092C20.9787 17.2343 21.0266 17.0363 20.9407 16.8105C20.2591 15.0204 18.5272 13.7501 16.5003 13.75ZM9.75032 8C9.75032 7.30972 9.19057 6.75013 8.50032 6.75C7.80996 6.75 7.25032 7.30964 7.25032 8C7.25032 8.69036 7.80996 9.25 8.50032 9.25C9.19057 9.24987 9.75032 8.69028 9.75032 8ZM18.2503 7.5C18.2503 6.53358 17.4667 5.75013 16.5003 5.75C15.5338 5.75 14.7503 6.5335 14.7503 7.5C14.7503 8.4665 15.5338 9.25 16.5003 9.25C17.4667 9.24987 18.2503 8.46642 18.2503 7.5ZM11.2503 8C11.2503 9.51871 10.019 10.7499 8.50032 10.75C6.98154 10.75 5.75032 9.51878 5.75032 8C5.75032 6.48122 6.98154 5.25 8.50032 5.25C10.019 5.25013 11.2503 6.48129 11.2503 8ZM19.7503 7.5C19.7503 9.29485 18.2951 10.7499 16.5003 10.75C14.7054 10.75 13.2503 9.29493 13.2503 7.5C13.2503 5.70507 14.7054 4.25 16.5003 4.25C18.2951 4.25013 19.7503 5.70515 19.7503 7.5Z",
  starred:
    "M12.0002 2.25C12.3031 2.25 12.5762 2.43225 12.6925 2.71191L14.8966 8.01172L20.6193 8.47168C20.9213 8.49589 21.179 8.69918 21.2726 8.9873C21.3661 9.27522 21.2777 9.591 21.048 9.78809L16.6877 13.5234L18.0197 19.1074C18.0898 19.402 17.9756 19.7097 17.7306 19.8877C17.4856 20.0656 17.158 20.0787 16.8996 19.9209L12.0002 16.9287L7.10074 19.9209C6.84228 20.0787 6.51473 20.0656 6.26968 19.8877C6.02466 19.7097 5.91046 19.402 5.98062 19.1074L7.31168 13.5234L2.9523 9.78809C2.72256 9.591 2.63418 9.27522 2.72769 8.9873C2.82131 8.69918 3.07903 8.49589 3.38101 8.47168L9.10269 8.01172L11.3078 2.71191L11.3586 2.6123C11.4929 2.38986 11.735 2.25 12.0002 2.25ZM10.3117 9.01172C10.2036 9.27112 9.95993 9.44813 9.67984 9.4707L5.298 9.82227L8.63589 12.6816C8.84949 12.8646 8.94334 13.1522 8.87808 13.4258L7.85757 17.7002L11.6095 15.4102L11.7023 15.3623C11.9241 15.2662 12.1809 15.282 12.3908 15.4102L16.1418 17.7002L15.1222 13.4258C15.057 13.1522 15.1508 12.8646 15.3644 12.6816L18.7013 9.82227L14.3205 9.4707C14.0404 9.44813 13.7967 9.27111 13.6886 9.01172L12.0002 4.95215L10.3117 9.01172Z",
  user: "M12.0006 12.25C15.523 12.2502 18.4949 14.5997 19.4381 17.8154C19.6746 18.6221 19.4321 19.3825 18.943 19.917C18.465 20.4392 17.7545 20.7499 17.0006 20.75H7.0006C6.24652 20.75 5.53531 20.4394 5.05724 19.917C4.56817 19.3826 4.32563 18.622 4.56212 17.8154C5.50533 14.5996 8.47805 12.25 12.0006 12.25ZM12.0006 13.75C9.16185 13.75 6.76251 15.6431 6.00158 18.2373C5.92731 18.4905 5.9923 18.717 6.16369 18.9043C6.34615 19.1037 6.65013 19.25 7.0006 19.25H17.0006C17.3509 19.2499 17.6541 19.1036 17.8365 18.9043C18.008 18.717 18.0729 18.4905 17.9986 18.2373C17.2377 15.6432 14.8392 13.7502 12.0006 13.75ZM14.2506 7C14.2506 5.75749 13.2431 4.75021 12.0006 4.75C10.758 4.75 9.7506 5.75736 9.7506 7C9.7506 8.24264 10.758 9.25 12.0006 9.25C13.2431 9.24979 14.2506 8.24251 14.2506 7ZM15.7506 7C15.7506 9.07094 14.0715 10.7498 12.0006 10.75C9.92953 10.75 8.2506 9.07107 8.2506 7C8.2506 4.92893 9.92953 3.25 12.0006 3.25C14.0715 3.25021 15.7506 4.92906 15.7506 7Z",
} as const;

type NavigationItem = {
  active?: boolean;
  href?: string;
  icon: SidebarIconPath;
  label: string;
  shortcut?: readonly string[];
};

const workspaces = [
  // A workspace with a logo shows it in its mark; one without shows its
  // initial.
  { icon: CircleDotIcon, initial: "A", name: "Acme", plan: "Pro plan" },
  { icon: null, initial: "L", name: "Acme Labs", plan: "Free plan" },
] as const;

const account = {
  avatar: "ME",
  email: "mara@example.com",
  name: "Mara Ellis",
};

const recentSpace = "Product roadmap";

const mainNavigation: NavigationItem[] = [
  {
    href: "#dashboard",
    icon: sidebarIconPaths.dashboard,
    label: "Dashboard",
  },
  {
    href: "#search",
    icon: sidebarIconPaths.search,
    label: "Search",
    shortcut: ["⌘", "K"],
  },
  {
    href: "#resources",
    icon: sidebarIconPaths.resources,
    label: "Resources",
  },
  {
    href: "#connectors",
    icon: sidebarIconPaths.connectors,
    label: "Connectors",
  },
];

const workspaceNavigation: NavigationItem[] = [
  {
    href: "#spaces",
    icon: sidebarIconPaths.projects,
    label: "All spaces",
  },
  { href: "#starred", icon: sidebarIconPaths.starred, label: "Starred" },
  {
    href: "#created-by-me",
    icon: sidebarIconPaths.user,
    label: "Created by me",
  },
  {
    href: "#shared-with-me",
    icon: sidebarIconPaths.shared,
    label: "Shared with me",
  },
];

type SidebarPatternLayout = "inset" | "push";

function ShellExample({
  layout,
}: {
  layout: SidebarPatternLayout;
}): React.ReactElement {
  const push = layout === "push";

  return (
    <div
      className="relative h-[640px] w-full overflow-hidden bg-[var(--sp-rw-bg-base)] [transform:translateZ(0)]"
      data-pattern="sidebar-workspace-example"
      data-sidebar-layout={layout}
    >
      <SidebarProvider className="h-full min-h-0!" layout={layout} defaultOpen>
        <Sidebar className="h-full!" collapsible="icon">
          <SidebarChrome layout={layout} />
        </Sidebar>

        <SidebarInset className="overflow-clip">
          <header className="flex h-12 shrink-0 items-center gap-2 px-4">
            <SidebarTrigger
              aria-label="Toggle sidebar"
              className={push ? undefined : "md:hidden"}
              size="icon-md"
            />
            <span className="text-sm text-[var(--sp-rw-fg-tertiary)]">
              Workspace
            </span>
            <span
              aria-hidden="true"
              className="text-[var(--sp-rw-fg-quaternary)]"
            >
              /
            </span>
            <span className="font-medium text-sm">Dashboard</span>
          </header>

          <div className="flex flex-1 flex-col gap-5 overflow-auto p-6">
            <div>
              <h2 className="font-medium text-xl">Welcome back</h2>
              <p className="mt-1 text-sm text-[var(--sp-rw-fg-tertiary)]">
                Continue building your Acme workspace.
              </p>
            </div>
            <Card className="h-28" />
            <div className="grid gap-4 sm:grid-cols-3">
              <Card className="h-32" />
              <Card className="h-32" />
              <Card className="h-32" />
            </div>
            <Card className="h-40" />
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}

export default function Example() {
  return <ShellExample layout="push" />;
}

type SidebarHeaderSlot = React.ComponentType<
  React.ComponentProps<typeof SidebarHeader>
>;

function SidebarChrome({
  Header = SidebarHeader,
  layout = "inset",
}: {
  Header?: SidebarHeaderSlot;
  layout?: SidebarPatternLayout;
} = {}): React.ReactElement {
  const [activeItem, setActiveItem] = React.useState("Dashboard");

  return (
    <>
      {/* The first row holds the logo, and the collapse button in the
          inset layout; it keeps the top bar's height beside it. */}
      <Header className="flex-row items-center justify-between gap-1">
        {/* On the collapsed rail the logo's mark expands the sidebar, at the
            height of the collapse button; the workspace below keeps its menu. */}
        <SidebarMenu className="min-w-0 flex-1">
          <SidebarMenuItem expandTrigger>
            <SidebarBrand render={<a href="#home" />}>
              <NorthlightMark />
              Northlight
            </SidebarBrand>
          </SidebarMenuItem>
        </SidebarMenu>
        {layout === "inset" ? <InsetSidebarToggle /> : null}
      </Header>

      {/* The workspace switcher is a row of its own outside SidebarContent,
          so it stays in place while the navigation scrolls. */}
      <SidebarHeader>
        <SidebarMenu>
          <WorkspaceSwitcher />
        </SidebarMenu>
      </SidebarHeader>

      <SidebarContent>
        <SidebarGroup>
          <SidebarNavigation
            activeItem={activeItem}
            items={mainNavigation}
            onActiveItemChange={setActiveItem}
          />
        </SidebarGroup>

        <SidebarGroup>
          <SidebarGroupLabel>Workspace</SidebarGroupLabel>
          <WorkspaceNavigation
            activeItem={activeItem}
            onActiveItemChange={setActiveItem}
          />
        </SidebarGroup>

        <RecentSpaces
          activeItem={activeItem}
          onActiveItemChange={setActiveItem}
        />
      </SidebarContent>

      <SidebarFooter>
        <UpgradeCard />
        <FooterNavigation />
      </SidebarFooter>

      <SidebarRail resizable />
    </>
  );
}

function InsetSidebarToggle(): React.ReactElement {
  return (
    <SidebarTrigger
      aria-label="Collapse sidebar"
      className="group-data-[collapsible=icon]:hidden"
      size="icon-md"
    />
  );
}

/** A neutral placeholder logo: a four-point star in a rounded square. */
function NorthlightMark(): React.ReactElement {
  return (
    <svg aria-hidden="true" className="size-5" viewBox="0 0 20 20">
      <rect
        className="fill-[var(--sp-rw-bg-inverse)]"
        height="20"
        rx="6"
        width="20"
      />
      <path
        className="fill-[var(--sp-rw-fg-inverse)]"
        d="M10 4.5 11.4 8.6 15.5 10 11.4 11.4 10 15.5 8.6 11.4 4.5 10 8.6 8.6Z"
      />
    </svg>
  );
}

function SidebarNavigation({
  activeItem,
  items,
  onActiveItemChange,
}: {
  activeItem: string;
  items: NavigationItem[];
  onActiveItemChange: (label: string) => void;
}): React.ReactElement {
  return (
    <SidebarMenu className="gap-px">
      {items.map((item) => {
        return (
          <SidebarMenuItem key={item.label}>
            <SidebarMenuButton
              active={activeItem === item.label}
              onClick={() => onActiveItemChange(item.label)}
              render={item.href ? <a href={item.href} /> : undefined}
              tooltip={item.label}
              type={item.href ? undefined : "button"}
            >
              <SidebarIcon className="size-5 shrink-0" iconPath={item.icon} />
              <span>{item.label}</span>
              {item.shortcut ? (
                <span className="ms-auto flex gap-px">
                  {item.shortcut.map((key) => (
                    <Kbd key={key}>{key}</Kbd>
                  ))}
                </span>
              ) : null}
            </SidebarMenuButton>
          </SidebarMenuItem>
        );
      })}
    </SidebarMenu>
  );
}

function WorkspaceNavigation({
  activeItem,
  onActiveItemChange,
}: {
  activeItem: string;
  onActiveItemChange: (label: string) => void;
}): React.ReactElement {
  return (
    <SidebarMenu className="gap-px">
      {workspaceNavigation.map((item, index) => (
        <WorkspaceFolderRow
          active={activeItem === item.label}
          hasActions={index === 0}
          item={item}
          key={item.label}
          onSelect={() => onActiveItemChange(item.label)}
        />
      ))}
      <LibraryNavigation
        activeItem={activeItem}
        onActiveItemChange={onActiveItemChange}
      />
    </SidebarMenu>
  );
}

function LibraryNavigation({
  activeItem,
  onActiveItemChange,
}: {
  activeItem: string;
  onActiveItemChange: (label: string) => void;
}): React.ReactElement {
  const { isMobile, state, setOpen } = useSidebar();

  return (
    <Collapsible
      onOpenChange={(open) => {
        if (open && !isMobile && state === "collapsed") setOpen(true);
      }}
      render={<SidebarMenuItem />}
    >
      <CollapsibleTrigger render={<SidebarMenuButton tooltip="Library" />}>
        <LibraryIcon className="size-5" />
        <span>Library</span>
        <span className="ms-1 flex shrink-0 items-center group-data-[collapsible=icon]:hidden">
          <ChevronDownIcon
            className="m-0! size-4"
            data-slot="collapsible-chevron"
          />
        </span>
      </CollapsibleTrigger>
      <CollapsiblePanel>
        <SidebarMenuSub>
          {[
            { label: "Documents", href: "#documents" },
            { label: "Templates", href: "#templates" },
            { label: "Media", href: "#media" },
          ].map((item) => (
            <SidebarMenuSubItem key={item.label}>
              <SidebarMenuSubButton
                active={activeItem === item.label}
                onClick={() => onActiveItemChange(item.label)}
                render={<a href={item.href} />}
              >
                {item.label}
              </SidebarMenuSubButton>
            </SidebarMenuSubItem>
          ))}
        </SidebarMenuSub>
      </CollapsiblePanel>
    </Collapsible>
  );
}

function WorkspaceFolderRow({
  active,
  hasActions,
  item,
  onSelect,
}: {
  active: boolean;
  hasActions: boolean;
  item: NavigationItem;
  onSelect: () => void;
}): React.ReactElement {
  return (
    <SidebarMenuItem className="group/folder-row">
      <SidebarMenuButton
        active={active}
        onClick={onSelect}
        render={<a href={item.href} />}
        tooltip={item.label}
      >
        <SidebarIcon
          aria-hidden="true"
          className="size-5"
          iconPath={item.icon}
        />
        <span>{item.label}</span>
      </SidebarMenuButton>

      {hasActions ? (
        <DropdownMenu>
          <DropdownMenuTrigger
            render={
              <SidebarMenuAction
                aria-label={`Options for ${item.label}`}
                showOnHover
                type="button"
              />
            }
          >
            <SidebarIcon
              className="size-5 shrink-0 opacity-100"
              iconPath={sidebarIconPaths.ellipsis}
            />
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end" className="w-[180px]" sideOffset={4}>
            <DropdownMenuItem>
              <PlusIcon />
              Create space
            </DropdownMenuItem>
            <DropdownMenuItem>
              <FolderPlusIcon />
              Create folder
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      ) : null}
    </SidebarMenuItem>
  );
}

// The switcher is a row of its own under the logo: the workspace's mark, its
// name over its plan and the up-down chevron. Two lines fill the 48 px row; on
// the collapsed rail it is the 36 px square with a 20 px mark, and the mark
// still opens the workspace menu there; the logo above expands the rail.
function WorkspaceSwitcher(): React.ReactElement {
  const { isMobile, state } = useSidebar();
  const collapsed = state === "collapsed" && !isMobile;
  const [activeName, setActiveName] = React.useState<string>(
    workspaces[0].name,
  );
  const active =
    workspaces.find((workspace) => workspace.name === activeName) ??
    workspaces[0];

  return (
    <SidebarMenuItem>
      <DropdownMenu>
        <DropdownMenuTrigger
          render={
            <SidebarMenuButton
              aria-label={`Open workspace switcher for ${active.name}`}
              size={collapsed ? "lg" : "xl"}
              type="button"
              variant="workspace"
            />
          }
        >
          <Avatar
            aria-hidden="true"
            className={collapsed ? "size-5 rounded-md" : "size-9 rounded-md"}
          >
            <AvatarFallback>
              {active.icon ? (
                <active.icon className="size-3/5" />
              ) : (
                active.initial
              )}
            </AvatarFallback>
          </Avatar>
          <SidebarMenuButtonText>
            <SidebarMenuButtonTitle>{active.name}</SidebarMenuButtonTitle>
            <SidebarMenuButtonDescription>
              {active.plan}
            </SidebarMenuButtonDescription>
          </SidebarMenuButtonText>
          <ChevronsUpDownIcon className="size-4 text-[var(--sp-rw-fg-tertiary)]" />
        </DropdownMenuTrigger>
        <DropdownMenuContent
          align="start"
          className={collapsed ? sidebarMenuPopupWidthClassName : "w-[280px]"}
          side="bottom"
          sideOffset={4}
        >
          <DropdownMenuGroup>
            <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
            <DropdownMenuRadioGroup
              onValueChange={(name) => setActiveName(String(name))}
              value={active.name}
            >
              {workspaces.map((workspace) => (
                <DropdownMenuRadioItem
                  closeOnClick
                  indicatorPosition="end"
                  key={workspace.name}
                  value={workspace.name}
                >
                  <Avatar aria-hidden="true" className="size-6 rounded-md">
                    <AvatarFallback>
                      {workspace.icon ? (
                        <workspace.icon className="size-3/5" />
                      ) : (
                        workspace.initial
                      )}
                    </AvatarFallback>
                  </Avatar>
                  {workspace.name}
                </DropdownMenuRadioItem>
              ))}
            </DropdownMenuRadioGroup>
          </DropdownMenuGroup>
          <DropdownMenuSeparator />
          <DropdownMenuItem>
            <PlusIcon />
            <span>New workspace</span>
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </SidebarMenuItem>
  );
}

function RecentSpaces({
  activeItem,
  onActiveItemChange,
}: {
  activeItem: string;
  onActiveItemChange: (label: string) => void;
}): React.ReactElement {
  return (
    <SidebarGroup className="group-data-[collapsible=icon]:hidden">
      <SidebarGroupLabel>Recently used</SidebarGroupLabel>
      <SidebarMenu>
        <SidebarMenuItem className="group/project-row">
          <SidebarMenuButton
            active={activeItem === recentSpace}
            onClick={() => onActiveItemChange(recentSpace)}
            render={<a href="#recent-space" />}
            tooltip={recentSpace}
          >
            <span>{recentSpace}</span>
          </SidebarMenuButton>
          <DropdownMenu>
            <DropdownMenuTrigger
              render={
                <SidebarMenuAction
                  aria-label={`More options for ${recentSpace}`}
                  showOnHover
                  type="button"
                />
              }
            >
              <SidebarIcon
                className="size-5 shrink-0 opacity-100"
                iconPath={sidebarIconPaths.ellipsis}
              />
            </DropdownMenuTrigger>
            <DropdownMenuContent
              align="end"
              className="w-[198px]"
              side="right"
              sideOffset={4}
            >
              <DropdownMenuItem>
                <StarOffIcon />
                Unstar
              </DropdownMenuItem>
              <DropdownMenuItem>
                <FolderInputIcon />
                Move to folder
              </DropdownMenuItem>
              <DropdownMenuItem>
                <CopyIcon />
                Duplicate
              </DropdownMenuItem>
              <DropdownMenuItem>
                <PencilIcon />
                Rename
              </DropdownMenuItem>
              <DropdownMenuItem>
                <SettingsIcon />
                Settings
              </DropdownMenuItem>
              <DropdownMenuSeparator />
              <DropdownMenuItem variant="destructive">
                <Trash2Icon />
                Delete
              </DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        </SidebarMenuItem>
      </SidebarMenu>
    </SidebarGroup>
  );
}

function UpgradeCard(): React.ReactElement {
  return (
    <Card className="group-data-[collapsible=icon]:hidden">
      <CardPanel className="flex items-center justify-between gap-3">
        <span className="flex min-w-0 flex-col">
          <span className="truncate font-medium text-sm leading-5">
            Make room for more
          </span>
          <span className="truncate text-xs leading-4 text-[var(--sp-rw-fg-tertiary)]">
            Explore workspace plans
          </span>
        </span>
        <Button
          aria-label="Explore workspace plans"
          render={<a href="#billing" />}
          shape="pill"
          size="icon-md"
          variant="muted-accent"
        >
          <ArrowUpRightIcon />
        </Button>
      </CardPanel>
    </Card>
  );
}

function FooterNavigation(): React.ReactElement {
  const [theme, setTheme] = React.useState("system");

  // On the collapsed rail the Messages link stacks above the avatar in the
  // rail's 36 px square: the avatar keeps its place at the bottom, and both
  // stand on the rail's axis from the first frame of the collapse.
  return (
    <div className="flex items-center justify-between group-data-[collapsible=icon]:w-(--sp-rw-control-lg) group-data-[collapsible=icon]:flex-col-reverse group-data-[collapsible=icon]:gap-2">
      <DropdownMenu>
        <Tooltip>
          <TooltipTrigger
            delay={0}
            render={
              <DropdownMenuTrigger
                render={
                  <Button
                    aria-label="Profile"
                    profile="avatar"
                    size="icon-md"
                    type="button"
                    variant="ghost"
                  />
                }
              />
            }
          >
            <Avatar>
              <AvatarFallback>{account.avatar}</AvatarFallback>
            </Avatar>
          </TooltipTrigger>
          <TooltipPopup side="right">Profile</TooltipPopup>
        </Tooltip>
        <DropdownMenuContent
          align="start"
          className="w-[220px]"
          side="top"
          sideOffset={4}
        >
          <DropdownMenuGroup>
            <DropdownMenuLabel className="flex items-center gap-2">
              <Avatar aria-hidden="true">
                <AvatarFallback>{account.avatar}</AvatarFallback>
              </Avatar>
              <span className="grid min-w-0">
                <span className="truncate font-medium text-sm text-[var(--sp-rw-fg-primary)]">
                  {account.name}
                </span>
                <span className="truncate font-normal text-xs text-[var(--sp-rw-fg-tertiary)]">
                  {account.email}
                </span>
              </span>
            </DropdownMenuLabel>
          </DropdownMenuGroup>
          <DropdownMenuSeparator />
          <DropdownMenuItem>
            <UserIcon />
            <span>Profile</span>
          </DropdownMenuItem>
          <DropdownMenuItem>
            <SettingsIcon />
            <span>Settings</span>
            <DropdownMenuShortcut>⌘,</DropdownMenuShortcut>
          </DropdownMenuItem>
          <DropdownMenuSub>
            <DropdownMenuSubTrigger>
              <SunMoonIcon />
              <span>Appearance</span>
            </DropdownMenuSubTrigger>
            <DropdownMenuSubContent className="w-[180px]">
              <DropdownMenuRadioGroup onValueChange={setTheme} value={theme}>
                <DropdownMenuRadioItem value="system">
                  System
                </DropdownMenuRadioItem>
                <DropdownMenuRadioItem value="light">
                  Light
                </DropdownMenuRadioItem>
                <DropdownMenuRadioItem value="dark">Dark</DropdownMenuRadioItem>
              </DropdownMenuRadioGroup>
            </DropdownMenuSubContent>
          </DropdownMenuSub>
          <DropdownMenuSeparator />
          <DropdownMenuSub>
            <DropdownMenuSubTrigger>
              <CircleHelpIcon />
              <span>Support</span>
            </DropdownMenuSubTrigger>
            <DropdownMenuSubContent className="w-[180px]">
              <DropdownMenuItem>
                <LifeBuoyIcon />
                Help center
              </DropdownMenuItem>
              <DropdownMenuItem>
                <HeadsetIcon />
                Contact support
              </DropdownMenuItem>
            </DropdownMenuSubContent>
          </DropdownMenuSub>
          <DropdownMenuSub>
            <DropdownMenuSubTrigger>
              <BookOpenIcon />
              <span>Documentation</span>
            </DropdownMenuSubTrigger>
            <DropdownMenuSubContent className="w-[180px]">
              <DropdownMenuItem>
                <BookMarkedIcon />
                Guides
              </DropdownMenuItem>
              <DropdownMenuItem>
                <LayersIcon />
                Changelog
              </DropdownMenuItem>
            </DropdownMenuSubContent>
          </DropdownMenuSub>
          <DropdownMenuItem>
            <UsersIcon />
            <span>Community</span>
            <ArrowUpRightIcon />
          </DropdownMenuItem>
          <DropdownMenuItem>
            <HouseIcon />
            <span>Homepage</span>
          </DropdownMenuItem>
          <DropdownMenuSeparator />
          <DropdownMenuItem>
            <LogOutIcon />
            <span>Sign out</span>
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>

      <Tooltip>
        <TooltipTrigger
          delay={0}
          render={
            <Button
              aria-label="Messages"
              profile="inline"
              profileTone="muted"
              render={<a href="#messages" />}
              size="xl"
              variant="text"
            />
          }
        >
          <SidebarIcon
            className="shrink-0 opacity-100"
            iconPath={sidebarIconPaths.inbox}
          />
        </TooltipTrigger>
        <TooltipPopup side="right">Messages</TooltipPopup>
      </Tooltip>
    </div>
  );
}

Open the full-width push example.

Layout sidebars retain their leading 8 px gutter when switching to the mobile Sheet, matching the trailing gutter in their header, content and footer. The account trigger uses Button's profile="avatar" for its circular portrait and shared keyboard focus outline.

Header layouts

SidebarBrand holds the application's logo: its mark first, then its name or wordmark. It starts where the menu rows start their icons, and a header band around it keeps the height of a 48 px top bar beside it, also with a 32 px collapse button in the same row. Render it as a link with render, such as render={<a href="/" />}; it takes the kit's focus outline and no hover fill. On the collapsed icon rail it shows only its mark, in the rail's 36 px square like the row icons: the mark keeps its place while its name fades out with the rail's width, as the row labels do, and fades back in when the sidebar expands. A plain-text name gets a span of its own, so a long one ends with an ellipsis and keeps its full text in a title attribute, and on the rail it stays the link's accessible name.

The top of the sidebar takes one of three layouts. The examples use the inset layout, where the first row holds the collapse button; there, a row with the logo moves down with the framed surface beside it and stays level with the top bar on it.

Workspace on top

The workspace switcher and the collapse button share the first row. On the collapsed rail the switcher shows its company mark, which expands the sidebar through expandTrigger.

"use client";

import {
  ChevronDownIcon,
  CircleDotIcon,
  FolderIcon,
  HouseIcon,
  InboxIcon,
  type LucideIcon,
  OrbitIcon,
  PlusIcon,
  SearchIcon,
} from "lucide-react";
import * as React from "react";

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/siteplane/components/dropdown-menu";
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  sidebarMenuPopupWidthClassName,
  useSidebar,
} from "@/siteplane/components/sidebar";

const navigation = [
  { icon: HouseIcon, label: "Home" },
  { icon: SearchIcon, label: "Search" },
  { icon: InboxIcon, label: "Inbox" },
  { icon: FolderIcon, label: "Projects" },
] as const;

const workspaces: { icon: LucideIcon; name: string }[] = [
  { icon: CircleDotIcon, name: "Acme" },
  { icon: OrbitIcon, name: "Acme Labs" },
];

export default function Example() {
  const [active, setActive] = React.useState<string>("Home");

  return (
    <div className="relative h-[32rem] w-full overflow-hidden bg-[var(--sp-rw-bg-base)] [transform:translateZ(0)]">
      <SidebarProvider className="h-full min-h-0" layout="inset">
        <Sidebar className="h-full" collapsible="icon">
          {/* The workspace switcher and the collapse button share the first
              row. */}
          <SidebarHeader className="flex-row items-center gap-1">
            <SidebarMenu className="min-w-0 flex-1">
              <WorkspaceSwitcher />
            </SidebarMenu>
            <SidebarTrigger
              aria-label="Collapse sidebar"
              className="group-data-[collapsible=icon]:hidden"
              size="icon-md"
            />
          </SidebarHeader>
          <SidebarContent>
            <SidebarGroup>
              <SidebarMenu>
                {navigation.map(({ icon: Icon, label }) => (
                  <SidebarMenuItem key={label}>
                    <SidebarMenuButton
                      active={active === label}
                      onClick={() => setActive(label)}
                      tooltip={label}
                    >
                      <Icon />
                      <span>{label}</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroup>
          </SidebarContent>
          <SidebarRail />
        </Sidebar>
        <SidebarInset>
          <header className="flex h-12 shrink-0 items-center gap-2 px-4">
            <SidebarTrigger
              aria-label="Toggle sidebar"
              className="md:hidden"
              size="icon-md"
            />
            <h1 className="font-medium text-sm">{active}</h1>
          </header>
          <div className="p-4 text-sm text-[var(--sp-rw-fg-tertiary)]">
            Your workspace content.
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}

// The company mark and the workspace name in one row. On the collapsed rail
// the mark also expands the sidebar: hover or focus it to reveal the control.
function WorkspaceSwitcher(): React.ReactElement {
  const { isMobile, state } = useSidebar();
  const [activeName, setActiveName] = React.useState(workspaces[0]!.name);
  const active =
    workspaces.find((workspace) => workspace.name === activeName) ??
    workspaces[0]!;

  return (
    <SidebarMenuItem expandTrigger>
      <DropdownMenu>
        <DropdownMenuTrigger
          render={
            <SidebarMenuButton
              aria-label={`Open workspace switcher for ${active.name}`}
              type="button"
              variant="workspace"
            />
          }
        >
          <CompanyMark icon={active.icon} />
          <span>{active.name}</span>
          <ChevronDownIcon className="ms-auto size-4 text-[var(--sp-rw-fg-tertiary)]" />
        </DropdownMenuTrigger>
        <DropdownMenuContent
          align="start"
          className={
            state === "collapsed" && !isMobile
              ? sidebarMenuPopupWidthClassName
              : "w-64"
          }
          side="bottom"
          sideOffset={4}
        >
          <DropdownMenuGroup>
            <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
            <DropdownMenuRadioGroup
              onValueChange={(name) => setActiveName(String(name))}
              value={active.name}
            >
              {workspaces.map((workspace) => (
                <DropdownMenuRadioItem
                  closeOnClick
                  indicatorPosition="end"
                  key={workspace.name}
                  value={workspace.name}
                >
                  <CompanyMark className="size-6" icon={workspace.icon} />
                  {workspace.name}
                </DropdownMenuRadioItem>
              ))}
            </DropdownMenuRadioGroup>
          </DropdownMenuGroup>
          <DropdownMenuSeparator />
          <DropdownMenuItem>
            <PlusIcon />
            <span>New workspace</span>
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </SidebarMenuItem>
  );
}

function CompanyMark({
  className = "size-5",
  icon: Icon,
}: {
  className?: string;
  icon: LucideIcon;
}): React.ReactElement {
  return (
    <span
      aria-hidden="true"
      className={`flex shrink-0 items-center justify-center rounded-full bg-[var(--sp-rw-bg-inverse)] text-[var(--sp-rw-fg-inverse)] ${className}`}
    >
      <Icon className="size-3/5" />
    </span>
  );
}

Logo above the workspace

The first row holds the logo and the collapse button; in the push layout the top bar's button toggles the sidebar and the first row holds the logo alone. The workspace switcher follows in a SidebarHeader of its own, outside SidebarContent, so it stays in place while the navigation scrolls. It shows the workspace's mark and its name over its plan, in a two-line row. On the collapsed rail the logo shows its mark, and the collapse button hides: the logo sits in a SidebarMenuItem with expandTrigger, so hovering or focusing its mark reveals the expand control at the height the collapse button had. The workspace mark below keeps opening the workspace menu. The examples at the top of this page use this layout.

"use client";

import {
  ChevronsUpDownIcon,
  CircleDotIcon,
  FolderIcon,
  HouseIcon,
  InboxIcon,
  PlusIcon,
  SearchIcon,
} from "lucide-react";
import * as React from "react";

import { Avatar, AvatarFallback } from "@/siteplane/components/avatar";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/siteplane/components/dropdown-menu";
import {
  Sidebar,
  SidebarBrand,
  SidebarContent,
  SidebarGroup,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuButtonDescription,
  SidebarMenuButtonText,
  SidebarMenuButtonTitle,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  sidebarMenuPopupWidthClassName,
  useSidebar,
} from "@/siteplane/components/sidebar";

const navigation = [
  { icon: HouseIcon, label: "Home" },
  { icon: SearchIcon, label: "Search" },
  { icon: InboxIcon, label: "Inbox" },
  { icon: FolderIcon, label: "Projects" },
] as const;

const workspaces = [
  // A workspace with a logo shows it in its mark; one without shows its
  // initial.
  { icon: CircleDotIcon, initial: "A", name: "Acme", plan: "Pro plan" },
  { icon: null, initial: "L", name: "Acme Labs", plan: "Free plan" },
] as const;

export default function Example() {
  const [active, setActive] = React.useState<string>("Home");

  return (
    <div className="relative h-[32rem] w-full overflow-hidden bg-[var(--sp-rw-bg-base)] [transform:translateZ(0)]">
      <SidebarProvider className="h-full min-h-0" layout="inset">
        <Sidebar className="h-full" collapsible="icon">
          {/* The first row: the logo and the collapse button, as tall as the
              top bar beside it. */}
          <SidebarHeader className="flex-row items-center justify-between gap-1">
            {/* On the collapsed rail the logo's mark expands the sidebar, at the
                height of the collapse button; the workspace below keeps its menu. */}
            <SidebarMenu className="min-w-0 flex-1">
              <SidebarMenuItem expandTrigger>
                <SidebarBrand render={<a href="#home" />}>
                  <NorthlightMark />
                  Northlight
                </SidebarBrand>
              </SidebarMenuItem>
            </SidebarMenu>
            <SidebarTrigger
              aria-label="Collapse sidebar"
              className="group-data-[collapsible=icon]:hidden"
              size="icon-md"
            />
          </SidebarHeader>
          {/* The workspace switcher is a row of its own outside
              SidebarContent, so it stays in place while the navigation
              scrolls. */}
          <SidebarHeader>
            <SidebarMenu>
              <WorkspaceSwitcher />
            </SidebarMenu>
          </SidebarHeader>
          <SidebarContent>
            <SidebarGroup>
              <SidebarMenu>
                {navigation.map(({ icon: Icon, label }) => (
                  <SidebarMenuItem key={label}>
                    <SidebarMenuButton
                      active={active === label}
                      onClick={() => setActive(label)}
                      tooltip={label}
                    >
                      <Icon />
                      <span>{label}</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroup>
          </SidebarContent>
          <SidebarRail />
        </Sidebar>
        <SidebarInset>
          <header className="flex h-12 shrink-0 items-center gap-2 px-4">
            <SidebarTrigger
              aria-label="Toggle sidebar"
              className="md:hidden"
              size="icon-md"
            />
            <h1 className="font-medium text-sm">{active}</h1>
          </header>
          <div className="p-4 text-sm text-[var(--sp-rw-fg-tertiary)]">
            Your workspace content.
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}

// The workspace's mark, its name over its plan and the up-down chevron. Two
// lines fill the 48 px row; on the collapsed rail it is the 36 px square with
// a 20 px mark, and the mark still opens the workspace menu there; the logo
// above expands the rail.
function WorkspaceSwitcher(): React.ReactElement {
  const { isMobile, state } = useSidebar();
  const collapsed = state === "collapsed" && !isMobile;
  const [activeName, setActiveName] = React.useState<string>(
    workspaces[0].name,
  );
  const active =
    workspaces.find((workspace) => workspace.name === activeName) ??
    workspaces[0];

  return (
    <SidebarMenuItem>
      <DropdownMenu>
        <DropdownMenuTrigger
          render={
            <SidebarMenuButton
              aria-label={`Open workspace switcher for ${active.name}`}
              size={collapsed ? "lg" : "xl"}
              type="button"
              variant="workspace"
            />
          }
        >
          <Avatar
            aria-hidden="true"
            className={collapsed ? "size-5 rounded-md" : "size-9 rounded-md"}
          >
            <AvatarFallback>
              {active.icon ? (
                <active.icon className="size-3/5" />
              ) : (
                active.initial
              )}
            </AvatarFallback>
          </Avatar>
          <SidebarMenuButtonText>
            <SidebarMenuButtonTitle>{active.name}</SidebarMenuButtonTitle>
            <SidebarMenuButtonDescription>
              {active.plan}
            </SidebarMenuButtonDescription>
          </SidebarMenuButtonText>
          <ChevronsUpDownIcon className="size-4 text-[var(--sp-rw-fg-tertiary)]" />
        </DropdownMenuTrigger>
        <DropdownMenuContent
          align="start"
          className={collapsed ? sidebarMenuPopupWidthClassName : "w-64"}
          side="bottom"
          sideOffset={4}
        >
          <DropdownMenuGroup>
            <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
            <DropdownMenuRadioGroup
              onValueChange={(name) => setActiveName(String(name))}
              value={active.name}
            >
              {workspaces.map((workspace) => (
                <DropdownMenuRadioItem
                  closeOnClick
                  indicatorPosition="end"
                  key={workspace.name}
                  value={workspace.name}
                >
                  <Avatar aria-hidden="true" className="size-6 rounded-md">
                    <AvatarFallback>
                      {workspace.icon ? (
                        <workspace.icon className="size-3/5" />
                      ) : (
                        workspace.initial
                      )}
                    </AvatarFallback>
                  </Avatar>
                  {workspace.name}
                </DropdownMenuRadioItem>
              ))}
            </DropdownMenuRadioGroup>
          </DropdownMenuGroup>
          <DropdownMenuSeparator />
          <DropdownMenuItem>
            <PlusIcon />
            <span>New workspace</span>
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
    </SidebarMenuItem>
  );
}

/** A neutral placeholder logo: a four-point star in a rounded square. */
function NorthlightMark(): React.ReactElement {
  return (
    <svg aria-hidden="true" className="size-5" viewBox="0 0 20 20">
      <rect
        className="fill-[var(--sp-rw-bg-inverse)]"
        height="20"
        rx="6"
        width="20"
      />
      <path
        className="fill-[var(--sp-rw-fg-inverse)]"
        d="M10 4.5 11.4 8.6 15.5 10 11.4 11.4 10 15.5 8.6 11.4 4.5 10 8.6 8.6Z"
      />
    </svg>
  );
}

Logo only

The first row holds the logo and the collapse button, without a workspace switcher. Put SidebarBrand in a SidebarMenuItem with expandTrigger, as in the layout above: on the collapsed rail, hovering or focusing the logo's mark reveals the expand control in its place, so the rail opens again from its first row.

"use client";

import { FolderIcon, HouseIcon, InboxIcon, SearchIcon } from "lucide-react";
import * as React from "react";

import {
  Sidebar,
  SidebarBrand,
  SidebarContent,
  SidebarGroup,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
} from "@/siteplane/components/sidebar";

const navigation = [
  { icon: HouseIcon, label: "Home" },
  { icon: SearchIcon, label: "Search" },
  { icon: InboxIcon, label: "Inbox" },
  { icon: FolderIcon, label: "Projects" },
] as const;

export default function Example() {
  const [active, setActive] = React.useState<string>("Home");

  return (
    <div className="relative h-[32rem] w-full overflow-hidden bg-[var(--sp-rw-bg-base)] [transform:translateZ(0)]">
      <SidebarProvider className="h-full min-h-0" layout="inset">
        <Sidebar className="h-full" collapsible="icon">
          {/* The logo and the collapse button, as tall as the top bar beside
              them. With expandTrigger, the logo's mark also expands the
              collapsed rail: hover or focus it to reveal the control. */}
          <SidebarHeader className="flex-row items-center gap-1">
            <SidebarMenu className="min-w-0 flex-1">
              <SidebarMenuItem expandTrigger>
                <SidebarBrand render={<a href="#home" />}>
                  <NorthlightMark />
                  Northlight
                </SidebarBrand>
              </SidebarMenuItem>
            </SidebarMenu>
            <SidebarTrigger
              aria-label="Collapse sidebar"
              className="group-data-[collapsible=icon]:hidden"
              size="icon-md"
            />
          </SidebarHeader>
          <SidebarContent>
            <SidebarGroup>
              <SidebarMenu>
                {navigation.map(({ icon: Icon, label }) => (
                  <SidebarMenuItem key={label}>
                    <SidebarMenuButton
                      active={active === label}
                      onClick={() => setActive(label)}
                      tooltip={label}
                    >
                      <Icon />
                      <span>{label}</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroup>
          </SidebarContent>
          <SidebarRail />
        </Sidebar>
        <SidebarInset>
          <header className="flex h-12 shrink-0 items-center gap-2 px-4">
            <SidebarTrigger
              aria-label="Toggle sidebar"
              className="md:hidden"
              size="icon-md"
            />
            <h1 className="font-medium text-sm">{active}</h1>
          </header>
          <div className="p-4 text-sm text-[var(--sp-rw-fg-tertiary)]">
            Your workspace content.
          </div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}

/** A neutral placeholder logo: a four-point star in a rounded square. */
function NorthlightMark(): React.ReactElement {
  return (
    <svg aria-hidden="true" className="size-5" viewBox="0 0 20 20">
      <rect
        className="fill-[var(--sp-rw-bg-inverse)]"
        height="20"
        rx="6"
        width="20"
      />
      <path
        className="fill-[var(--sp-rw-fg-inverse)]"
        d="M10 4.5 11.4 8.6 15.5 10 11.4 11.4 10 15.5 8.6 11.4 4.5 10 8.6 8.6Z"
      />
    </svg>
  );
}

With a layout, the Sidebar reserves the leading 8 px gutter on its container, so SidebarHeader, SidebarGroup and SidebarFooter add only their trailing inset and keep their block padding. Do not override their padding to remove the leading side. SidebarHeader and SidebarFooter are bands with 8 px above and below, and SidebarFooter spaces its groups 16 px apart, like the groups of the content. SidebarContent scrolls between the two bands and keeps its trailing inset inside the scroll extent, so the last row never ends flush with, or under, the footer card.

A sidebar sits flat on the page background in both layouts, with no frame, border or shadow of its own; the main surface carries the edge. From the mobile breakpoint up, SidebarInset is the primary surface beside it, with a layout or without one, so the sidebar and the content never share one grey, and the cards on it are white with a visible hairline.

The examples show the logo of a fictional application, Northlight, at the top, the switcher for its Acme workspace under it, and the signed-in person (initials avatar, name and email in its menu) as the account at the bottom, so none of them repeats another.

SidebarMenuButton and SidebarMenuSubButton own their inline padding: 8 px on both sides, including text-only rows. Leading icons and trailing controls do not supply that spacing. Do not add compensating padding to label, shortcut or chevron wrappers. Place a 20 px icon or avatar directly before the label; do not wrap it in an additional spacing container. On the collapsed icon rail, main rows keep that same inset, so the icon stays where it was, and become squares as wide as the row is high. The rail is 52 px wide, which is the 8 px inset, the 36 px square and the 8 px inset again: the toggle, the workspace mark, every icon and the active pill share the rail's center axis. Header and footer items keep the inset they have in the expanded sidebar, as the rows do, so the width animation never moves them: a 36 px item, such as the logo's square or a row, stands on the axis, and a group of smaller controls takes the square's width on the rail (group-data-[collapsible=icon]:w-(--sp-rw-control-lg)) and centers them in it. The examples stack their Messages control above the account there, so the account stays at the bottom of the rail, where it was. Labels and trailing content fade out and remain available to assistive technology. A group label keeps its row, so the groups stay apart and the icons below it do not move. Explicitly sized icons use the same margin reset as default-sized icons.

A label that is too long for the sidebar ends with an ellipsis. In a menu row this holds for every text span after the row's icon or avatar, also when a chevron follows it, so the chevron stays in the row; kit parts with a slot of their own, such as an Avatar or a Badge, keep their size. A plain-text SidebarGroupLabel ends with an ellipsis as well and keeps its full text in a title attribute.

SidebarContent owns scrolling. With a layout it borrows the sidebar's leading gutter so the shared focus outline is not clipped, without changing row alignment; without a layout its scroll viewport stays inside the sidebar and the groups' own padding keeps the outline clear. Keep navigation groups inside that scroll area; another overflow container can clip the outline again.

Icon collapse and resize rail

Place an icon before each menu label. On the collapsed rail only the icon stays visible, and tooltip names the item on hover or keyboard focus.

SidebarRail is an 8 px strip along the whole outer edge of the sidebar, clear of its scrollbar. A click on it toggles the sidebar. With resizable, dragging it resizes the expanded sidebar between --sidebar-width-min and --sidebar-width-max (12rem and 24rem by default). The rail stays out of the Tab order, because SidebarTrigger and Ctrl/Cmd+B toggle the sidebar from the keyboard.

"use client";

import {
  Sidebar,
  SidebarBrand,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
} from "@/siteplane/components/sidebar";
import {
  CircleDotIcon,
  FolderIcon,
  HomeIcon,
  SettingsIcon,
} from "lucide-react";
import { useState } from "react";

export default function Example() {
  const [active, setActive] = useState("Overview");
  return (
    <div className="relative h-[32rem] w-full overflow-hidden [transform:translateZ(0)]">
      <SidebarProvider className="h-full min-h-0">
        <Sidebar className="h-full" collapsible="icon">
          <SidebarHeader>
            {/* The mark stays on the collapsed rail; the name leaves it. */}
            <SidebarBrand>
              <span
                aria-hidden="true"
                className="flex size-5 shrink-0 items-center justify-center rounded-full bg-[var(--sp-rw-bg-inverse)] text-[var(--sp-rw-fg-inverse)]"
              >
                <CircleDotIcon className="size-3/5 opacity-100" />
              </span>
              Acme
            </SidebarBrand>
          </SidebarHeader>
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Workspace</SidebarGroupLabel>
              <SidebarMenu>
                {[
                  { label: "Overview", icon: HomeIcon },
                  { label: "Projects", icon: FolderIcon },
                  { label: "Settings", icon: SettingsIcon },
                ].map(({ label, icon: Icon }) => (
                  <SidebarMenuItem key={label}>
                    <SidebarMenuButton
                      tooltip={label}
                      active={active === label}
                      onClick={() => setActive(label)}
                    >
                      <Icon />
                      <span>{label}</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroup>
          </SidebarContent>
          <SidebarFooter>
            <span className="truncate px-2 text-xs text-muted-foreground transition-opacity duration-(--motion-duration-base) ease-(--motion-ease-linear) group-data-[collapsible=icon]:opacity-0">
              Acme workspace
            </span>
          </SidebarFooter>
          <SidebarRail resizable />
        </Sidebar>
        <SidebarInset>
          <header className="flex items-center gap-2 border-b p-3">
            <SidebarTrigger />
            <h1 className="font-semibold">{active}</h1>
          </header>
          <div className="p-4 text-sm">Your workspace content.</div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}

Add SidebarInput for search, SidebarMenuBadge for counts, SidebarMenuSub for nested pages and SidebarMenuSkeleton while items load. A menu button keeps room at its end for a badge or a SidebarMenuAction, so a long label truncates before it. SidebarMenuAction renders Button's bare icon action, an icon in a square target of the control size with no hover or pressed background. With showOnHover it shows only while its row is hovered or focused, or while its menu is open.

"use client";

import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSkeleton,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarSeparator,
} from "@/siteplane/components/sidebar";
import { FolderIcon } from "lucide-react";

export default function Example() {
  return (
    <div className="relative h-[32rem] w-full overflow-hidden">
      <SidebarProvider className="h-full min-h-0">
        <Sidebar collapsible="none">
          <SidebarHeader>
            <SidebarInput
              aria-label="Search projects"
              placeholder="Search projects…"
            />
          </SidebarHeader>
          <SidebarSeparator />
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Projects</SidebarGroupLabel>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuButton active render={<a href="#website" />}>
                    <FolderIcon />
                    <span>Website</span>
                  </SidebarMenuButton>
                  <SidebarMenuBadge>3</SidebarMenuBadge>
                  <SidebarMenuSub>
                    <SidebarMenuSubItem>
                      <SidebarMenuSubButton render={<a href="#design" />}>
                        Design
                      </SidebarMenuSubButton>
                    </SidebarMenuSubItem>
                    <SidebarMenuSubItem>
                      <SidebarMenuSubButton render={<a href="#development" />}>
                        Development
                      </SidebarMenuSubButton>
                    </SidebarMenuSubItem>
                  </SidebarMenuSub>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuSkeleton showIcon width="65%" />
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroup>
          </SidebarContent>
        </Sidebar>
        <SidebarInset>
          <header className="flex items-center gap-2 border-b p-3">
            <h1 className="font-semibold">Website</h1>
          </header>
          <div className="p-4 text-sm">Your project content.</div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}

SidebarSeparator is the kit's divider inside the sidebar's 8 px inset. It spans the band it sits in, also where the icon rail centers the band's items, and never runs past the sidebar's edge.

Two-line rows

A row can show a name over a quieter detail line, such as a workspace and its plan or an account and its email. Put SidebarMenuButtonText after the row's avatar or icon, with SidebarMenuButtonTitle and SidebarMenuButtonDescription inside it. The title keeps the row's text. The description is one kit text step smaller (12 px on an 18 px line, or 14 px on a 21 px line in an xl row) in the tertiary text color. Both stay on one line, end with an ellipsis when they are too long, and keep their full text in a title attribute when they are plain text. The text takes the row's free width, so a trailing chevron stays at the row's end. Two lines fill the 48 px row, so give the button size="xl". The collapsed icon rail holds 36 px squares with a 20 px icon: in a sidebar that collapses to it, give the row size="lg" and its avatar the 20 px of a row icon (size-5) while useSidebar().state is "collapsed", so the avatar stays on the rail's center axis. The row follows the switch at the sidebar's pace: its height, padding and text and the avatar's size change with the rail's width, so the rows below glide up instead of jumping. Reduced motion makes the change instant.

"use client";

import {
  ChevronsUpDownIcon,
  FolderIcon,
  HouseIcon,
  LogOutIcon,
  PlusIcon,
  SettingsIcon,
  UserIcon,
} from "lucide-react";
import { useState } from "react";

import { Avatar, AvatarFallback } from "@/siteplane/components/avatar";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/siteplane/components/dropdown-menu";
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuButtonDescription,
  SidebarMenuButtonText,
  SidebarMenuButtonTitle,
  SidebarMenuItem,
  SidebarProvider,
  SidebarSeparator,
} from "@/siteplane/components/sidebar";

const workspaces = [
  { name: "Northlight Studio", plan: "Pro plan", initial: "N" },
  { name: "Copperleaf Spa & Wellness", plan: "Free plan", initial: "C" },
] as const;

export default function Example() {
  const [workspace, setWorkspace] = useState<string>(workspaces[0].name);
  const current =
    workspaces.find((item) => item.name === workspace) ?? workspaces[0];

  return (
    <div className="relative h-[32rem] w-full overflow-hidden">
      <SidebarProvider className="h-full min-h-0">
        <Sidebar collapsible="none">
          <SidebarHeader>
            <SidebarMenu>
              <SidebarMenuItem>
                <DropdownMenu>
                  <DropdownMenuTrigger
                    render={
                      <SidebarMenuButton
                        aria-label={`Switch workspace, current: ${current.name}`}
                        size="xl"
                        type="button"
                        variant="workspace"
                      />
                    }
                  >
                    <Avatar aria-hidden="true" className="rounded-md">
                      <AvatarFallback>{current.initial}</AvatarFallback>
                    </Avatar>
                    <SidebarMenuButtonText>
                      <SidebarMenuButtonTitle>
                        {current.name}
                      </SidebarMenuButtonTitle>
                      <SidebarMenuButtonDescription>
                        {current.plan}
                      </SidebarMenuButtonDescription>
                    </SidebarMenuButtonText>
                    <ChevronsUpDownIcon />
                  </DropdownMenuTrigger>
                  <DropdownMenuContent align="start" className="w-64">
                    <DropdownMenuGroup>
                      <DropdownMenuLabel>Workspaces</DropdownMenuLabel>
                      <DropdownMenuRadioGroup
                        onValueChange={(value) => setWorkspace(String(value))}
                        value={workspace}
                      >
                        {workspaces.map((item) => (
                          <DropdownMenuRadioItem
                            closeOnClick
                            indicatorPosition="end"
                            key={item.name}
                            value={item.name}
                          >
                            <Avatar
                              aria-hidden="true"
                              className="size-5 rounded-md"
                            >
                              <AvatarFallback>{item.initial}</AvatarFallback>
                            </Avatar>
                            {item.name}
                          </DropdownMenuRadioItem>
                        ))}
                      </DropdownMenuRadioGroup>
                    </DropdownMenuGroup>
                    <DropdownMenuSeparator />
                    <DropdownMenuItem>
                      <PlusIcon />
                      New workspace
                    </DropdownMenuItem>
                  </DropdownMenuContent>
                </DropdownMenu>
              </SidebarMenuItem>
            </SidebarMenu>
          </SidebarHeader>
          <SidebarContent>
            <SidebarGroup>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuButton active render={<a href="#home" />}>
                    <HouseIcon />
                    <span>Home</span>
                  </SidebarMenuButton>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuButton render={<a href="#projects" />}>
                    <FolderIcon />
                    <span>Projects</span>
                  </SidebarMenuButton>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroup>
          </SidebarContent>
          <SidebarSeparator />
          <SidebarFooter>
            <SidebarMenu>
              <SidebarMenuItem>
                <DropdownMenu>
                  <DropdownMenuTrigger
                    render={
                      <SidebarMenuButton
                        aria-label="Account, Mara Ellis"
                        size="xl"
                        type="button"
                      />
                    }
                  >
                    <Avatar aria-hidden="true">
                      <AvatarFallback>ME</AvatarFallback>
                    </Avatar>
                    <SidebarMenuButtonText>
                      <SidebarMenuButtonTitle>
                        Mara Ellis
                      </SidebarMenuButtonTitle>
                      <SidebarMenuButtonDescription>
                        mara.ellis@northlight.example.com
                      </SidebarMenuButtonDescription>
                    </SidebarMenuButtonText>
                    <ChevronsUpDownIcon />
                  </DropdownMenuTrigger>
                  <DropdownMenuContent
                    align="start"
                    className="w-56"
                    side="top"
                  >
                    <DropdownMenuItem>
                      <UserIcon />
                      Profile
                    </DropdownMenuItem>
                    <DropdownMenuItem>
                      <SettingsIcon />
                      Settings
                    </DropdownMenuItem>
                    <DropdownMenuSeparator />
                    <DropdownMenuItem>
                      <LogOutIcon />
                      Sign out
                    </DropdownMenuItem>
                  </DropdownMenuContent>
                </DropdownMenu>
              </SidebarMenuItem>
            </SidebarMenu>
          </SidebarFooter>
        </Sidebar>
        <SidebarInset>
          <header className="flex items-center gap-2 border-b p-3">
            <h1 className="font-semibold">Home</h1>
          </header>
          <div className="p-4 text-sm">Your workspace content.</div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}

Right-to-left layouts

side is the inline start or end of the page. In a right-to-left page, side="left" puts the sidebar on the right, together with its gap, its rail and the sheet on small screens, and a resizable rail moves its edge the other way. Badges, group actions and the nested guide line sit at the row's inline end or start, collapsed-rail tooltips open toward the content, and the default trigger glyph mirrors.

Persist the state

On every real change of the desktop state, the provider writes it to a cookie named sidebar_state, for seven days. It never reads the cookie back, so read it where the page renders and pass it to defaultOpen; the first render then matches the last choice. In Next.js, a nested layout for the pages with the sidebar can read it on the server:

src/app/(app)/layout.tsx
import { cookies } from "next/headers";
import { SidebarProvider } from "@/siteplane/components/sidebar";

export default async function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  const cookieStore = await cookies();
  const defaultOpen = cookieStore.get("sidebar_state")?.value !== "false";
  return (
    <SidebarProvider defaultOpen={defaultOpen}>{children}</SidebarProvider>
  );
}

Set stateCookie to another name when several sidebars share a domain, for example an application sidebar and a settings sidebar, or to false to write no cookie.

Controlled desktop state

open and onOpenChange control desktop expansion. Mobile sheet state remains separate.

"use client";

import {
  Sidebar,
  SidebarBrand,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from "@/siteplane/components/sidebar";
import { FolderIcon, HomeIcon, SettingsIcon } from "lucide-react";
import { useState } from "react";

export default function Example() {
  const [active, setActive] = useState("Overview");
  const [open, setOpen] = useState(true);
  return (
    <div className="relative h-[32rem] w-full overflow-hidden [transform:translateZ(0)]">
      <SidebarProvider
        className="h-full min-h-0"
        open={open}
        onOpenChange={setOpen}
      >
        <Sidebar className="h-full">
          <SidebarHeader>
            <SidebarBrand>Acme</SidebarBrand>
          </SidebarHeader>
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Workspace</SidebarGroupLabel>
              <SidebarMenu>
                {[
                  { label: "Overview", icon: HomeIcon },
                  { label: "Projects", icon: FolderIcon },
                  { label: "Settings", icon: SettingsIcon },
                ].map(({ label, icon: Icon }) => (
                  <SidebarMenuItem key={label}>
                    <SidebarMenuButton
                      tooltip={label}
                      active={active === label}
                      onClick={() => setActive(label)}
                    >
                      <Icon />
                      <span>{label}</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroup>
          </SidebarContent>
          <SidebarFooter>
            <span className="truncate px-2 text-xs text-muted-foreground">
              Acme workspace
            </span>
          </SidebarFooter>
        </Sidebar>
        <SidebarInset>
          <header className="flex items-center gap-2 border-b p-3">
            <SidebarTrigger />
            <h1 className="font-semibold">{active}</h1>
          </header>
          <div className="p-4 text-sm">Your workspace content.</div>
        </SidebarInset>
      </SidebarProvider>
    </div>
  );
}

API reference

Component properties
PropTypeDefaultDescription
SidebarProvider.layout"inset" | "push"—Application layout; DashboardShell sets it.
SidebarProvider.defaultOpen / openbooleantrue / uncontrolledInitial or controlled desktop expansion.
SidebarProvider.mobileBreakpoint"md" | "lg""md"Switch to the mobile Sheet below 768 px (md) or 1024 px (lg), the widths of the md: and lg: utilities.
SidebarProvider.stateCookiestring | false"sidebar_state"Name of the cookie that keeps the desktop state, or false for no cookie. The provider writes it and never reads it.
SidebarProvider.keyboardShortcutbooleantrueRegister Ctrl/Cmd+B. Disable for an auxiliary navigation that must not compete with the application sidebar.
SidebarProvider.onOpenChange(open: boolean) => void—Desktop expansion changes.
Sidebar side / variant / collapsibleleft | right / sidebar | floating | inset / offcanvas | icon | noneleft / sidebar / offcanvasAttachment, surface, and collapse behavior; none remains static on mobile. left and right are the inline start and end, so a left sidebar sits on the right in a right-to-left page.
Sidebar.mobileVariantdefault | fullscreendefaultMobile navigation surface. default slides in from the sidebar's side at the mobile width (18rem); fullscreen fills the viewport and fades in from the top.
SidebarTriggerButton propsghost, square at the provider size (icon-lg)Toggles desktop or mobile state and announces it with aria-expanded and aria-controls; invokes supplied onClick before toggling.
SidebarRail.resizablebooleanfalsePointer resize within CSS width limits; rail is excluded from Tab navigation. Supplied onClick and onPointerDown run before the rail's own.
SidebarMenuItem.expandTriggerbooleanfalsePlaces SidebarTrigger over the item's row. On the collapsed icon rail, hover or focus swaps the row for the trigger, so a workspace mark also expands the sidebar. A SidebarBrand in the item steps aside the same way.
SidebarMenuItem.expandLabelstring"Expand sidebar"Accessible name of the expand trigger.
SidebarMenuButton.active / tooltipboolean / string | TooltipPopup propsfalse / —active shows the active style and sets aria-current="page" unless you pass aria-current, which shows the active style as well. isActive is a deprecated alias of active. tooltip names the item on the collapsed desktop rail.
SidebarMenuButton.size / variantsm | md | lg | xl / default | outline | workspaceprovider (lg) / defaultMenu density and treatment.
SidebarMenuButton.renderElement or render callbackbuttonRouter/link composition.
SidebarMenuButtonText / SidebarMenuButtonTitle / SidebarMenuButtonDescriptionspan props—A two-line row: the title in the row's text over a description one text step smaller in the tertiary color. Both truncate, and plain text keeps its full value in title unless you pass one.
SidebarMenuAction.showOnHoverbooleanfalseReveal action on hover/focus; accepts button useRender props.
SidebarMenuSkeleton.showIcon / width / sizeboolean / string / "sm" | "md" | "lg" | "xl"false / "72%" / provider (lg)Loading row shape, as high as a menu row of the same size.
SidebarMenuSubButton.size / active"sm" | "md" | "lg" | "xl" / booleanprovider (lg) / falseNested button dimensions/state; active also sets aria-current="page", and a passed aria-current shows the active style. isActive is a deprecated alias of active. Accepts ButtonBaseProps except size.
SidebarGroupLabel / SidebarGroupActionuseRender div / button propsdiv / buttonGroup labels and actions with render composition. A plain-text label ends with an ellipsis and keeps its full text in title.
SidebarInsetmain props—Application content beside the sidebar: the primary surface from the mobile breakpoint up, with a layout or without one. In the push layout it moves its content clear of the expanded sidebar.
SidebarBranduseRender div propsdivThe application's logo: its mark first, then its name or wordmark. It starts at the rows' 8 px icon inset and is as tall as the rail's 36 px square, reaching 2 px into the band's padding, so the band stays 48 px. On the collapsed icon rail only the mark shows, in that square, and keeps its place while the name fades out. A plain-text name ends with an ellipsis and keeps its full text in title. Render a link with render; in a SidebarMenuItem with expandTrigger it steps aside for the expand control on the rail.
SidebarHeader / SidebarFooter / SidebarContent / SidebarGroup / SidebarGroupContent / SidebarMenuBadgediv props—Layout parts; content supplies scrolling and space for the shared focus outline. Do not add a nested scrolling wrapper around menu groups.
SidebarMenu / SidebarMenuSub / SidebarMenuItem / SidebarMenuSubItemul / ul / li / li props—List semantics.
SidebarInput / SidebarSeparatorInput / Separator propsKit defaultsStyled input, and the kit divider inside the sidebar's 8 px inset.
useSidebar()SidebarContextPropsProvider requiredlayout, state, open, setOpen, openMobile, setOpenMobile, isMobile, mobileBreakpoint, toggleSidebar.
SidebarContextReact.Context<SidebarContextProps | null>nullPublic context, normally read through useSidebar.
sidebarMenuPopupWidthClassNamestring"w-56"Width utility for menus attached to the sidebar.

SidebarLayout is the exported "inset" | "push" union used by the provider and context.

Read the installed source.

Usage notes

The provider registers Ctrl/Cmd+B unless keyboardShortcut is false; the shortcut leaves the keys to a focused text field or editor, to input methods and to handlers that already took the event. Desktop changes write the state cookie (path /) for seven days, sidebar_state unless stateCookie names another or turns it off, using the Cookie Store API where the browser provides it and document.cookie otherwise; if cookies are blocked, the sidebar still toggles. A controlled state is written once the parent applies the change. Initial state is not read back automatically: a server or application must read the cookie and pass defaultOpen, as in Persist the state. A collapsed offcanvas sidebar leaves the tab order and the accessibility tree once it has slid out; its rail stays available to open it. Mobile navigation uses Sheet and starts closed; its dialog takes the Sidebar's aria-label as its title ("Sidebar" without one), and closing it returns focus to the SidebarTrigger that opened it. On desktop, id, aria-label, aria-labelledby and aria-describedby go to the aside landmark, while className, style and the other props go to the sidebar container; on mobile every prop reaches the sheet. Set mobileVariant="fullscreen" on Sidebar for a full-viewport navigation surface. Nested navigation pages can compose Sheet with variant="fullscreen"; the Sheet owns focus, Escape and return focus. Set mobileBreakpoint="lg" when a header switches to mobile navigation below 1024 px. SidebarTrigger accepts children to customize its icon; provide an accessible label when replacing the default content. --sidebar-width defaults to 16rem, icon width to 3.25rem, resize bounds to 12rem/24rem, and mobile width to 18rem. Set desktop custom properties through provider style. The bounded preview wrapper is included in the source; remove it for a full viewport application. Native element attributes and the linked Button, Input and Tooltip APIs apply to their respective parts.

In the complete examples, workspace selection and the active navigation item update locally. Replace the sample data and hash links with your routes, and connect search, account, billing and project actions to your application. Keyboard shortcut labels are illustrative; only Ctrl/Cmd+B is registered by the sidebar provider.

The preview runs in its own document viewport so navigation landmarks and responsive behavior stay independent of the docs.