Siteplane UI

Dashboard Shell

Compose a sidebar, header, and scrolling application content.

Markdown
"use client";

import {
  DashboardShell,
  DashboardShellHeader,
  DashboardShellInset,
  type DashboardShellLayout,
  DashboardShellMain,
  DashboardShellPrimary,
  DashboardShellSidebar,
  DashboardShellSidebarHeader,
  DashboardShellSidebarTrigger,
} from "@/siteplane/components/dashboard-shell";
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/siteplane/components/breadcrumb";
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 {
  SidebarBrand,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuButton,
  SidebarMenuButtonDescription,
  SidebarMenuButtonText,
  SidebarMenuButtonTitle,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  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: DashboardShellLayout;
}): React.ReactElement {
  return (
    <div
      className="relative h-[640px] w-full overflow-hidden bg-[var(--sp-rw-bg-base)] [transform:translateZ(0)]"
      data-pattern="dashboard-shell-demo"
      data-shell-layout={layout}
    >
      <DashboardShell className="h-full min-h-0!" defaultOpen layout={layout}>
        <DashboardShellSidebar className="h-full!">
          <SidebarChrome Header={DashboardShellSidebarHeader} layout={layout} />
        </DashboardShellSidebar>

        <DashboardShellInset>
          <DashboardShellHeader>
            <DashboardShellSidebarTrigger
              aria-label="Toggle sidebar"
              className={layout === "inset" ? "md:hidden" : undefined}
            />
            <Breadcrumb>
              <BreadcrumbList>
                <BreadcrumbItem>Workspace</BreadcrumbItem>
                <BreadcrumbSeparator />
                <BreadcrumbItem>
                  <BreadcrumbPage>Dashboard</BreadcrumbPage>
                </BreadcrumbItem>
              </BreadcrumbList>
            </Breadcrumb>
          </DashboardShellHeader>
          <DashboardShellPrimary aria-label="Workspace">
            <DashboardShellMain className="flex flex-col gap-5">
              <div>
                <h1 className="font-medium text-xl">Welcome back</h1>
                <p className="mt-1 text-muted-foreground text-sm">
                  Here is what changed in your Acme workspace this week.
                </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>
            </DashboardShellMain>
          </DashboardShellPrimary>
        </DashboardShellInset>
      </DashboardShell>
    </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/dashboard-shell

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/breadcrumb @siteplane/button @siteplane/card @siteplane/collapsible @siteplane/dropdown-menu @siteplane/kbd @siteplane/sidebar @siteplane/tooltip

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

pnpm dlx shadcn@4.18.0 add @siteplane/dashboard-shell-inset-example

Open the full-width interactive example. With layout="inset", the content sits on a framed surface and the sidebar collapses from its first row, next to the logo; when 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. See Sidebar for the navigation parts.

Usage

import {
  DashboardShell,
  DashboardShellSidebar,
  DashboardShellInset,
  DashboardShellHeader,
  DashboardShellMain,
  DashboardShellSidebarTrigger,
} from "@/siteplane/components/dashboard-shell";

<DashboardShell layout="inset">
  <DashboardShellSidebar />
  <DashboardShellInset>
    <DashboardShellHeader>
      <DashboardShellSidebarTrigger />
      Workspace
    </DashboardShellHeader>
    <DashboardShellMain>Content</DashboardShellMain>
  </DashboardShellInset>
</DashboardShell>;

Composition

DashboardShell
  DashboardShellSidebar
    DashboardShellSidebarHeader → SidebarBrand + DashboardShellSidebarTrigger
    SidebarHeader → SidebarMenu → SidebarMenuItem (workspace switcher)
    SidebarContent
  DashboardShellInset
    DashboardShellHeader → DashboardShellSidebarTrigger
    DashboardShellPrimary (optional)
      DashboardShellMain

Push layout

The default layout="push" runs the content on one unframed 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.

"use client";

import {
  DashboardShell,
  DashboardShellHeader,
  DashboardShellInset,
  type DashboardShellLayout,
  DashboardShellMain,
  DashboardShellPrimary,
  DashboardShellSidebar,
  DashboardShellSidebarHeader,
  DashboardShellSidebarTrigger,
} from "@/siteplane/components/dashboard-shell";
import {
  Breadcrumb,
  BreadcrumbItem,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
} from "@/siteplane/components/breadcrumb";
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 {
  SidebarBrand,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuAction,
  SidebarMenuButton,
  SidebarMenuButtonDescription,
  SidebarMenuButtonText,
  SidebarMenuButtonTitle,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  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: DashboardShellLayout;
}): React.ReactElement {
  return (
    <div
      className="relative h-[640px] w-full overflow-hidden bg-[var(--sp-rw-bg-base)] [transform:translateZ(0)]"
      data-pattern="dashboard-shell-demo"
      data-shell-layout={layout}
    >
      <DashboardShell className="h-full min-h-0!" defaultOpen layout={layout}>
        <DashboardShellSidebar className="h-full!">
          <SidebarChrome Header={DashboardShellSidebarHeader} layout={layout} />
        </DashboardShellSidebar>

        <DashboardShellInset>
          <DashboardShellHeader>
            <DashboardShellSidebarTrigger
              aria-label="Toggle sidebar"
              className={layout === "inset" ? "md:hidden" : undefined}
            />
            <Breadcrumb>
              <BreadcrumbList>
                <BreadcrumbItem>Workspace</BreadcrumbItem>
                <BreadcrumbSeparator />
                <BreadcrumbItem>
                  <BreadcrumbPage>Dashboard</BreadcrumbPage>
                </BreadcrumbItem>
              </BreadcrumbList>
            </Breadcrumb>
          </DashboardShellHeader>
          <DashboardShellPrimary aria-label="Workspace">
            <DashboardShellMain className="flex flex-col gap-5">
              <div>
                <h1 className="font-medium text-xl">Welcome back</h1>
                <p className="mt-1 text-muted-foreground text-sm">
                  Here is what changed in your Acme workspace this week.
                </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>
            </DashboardShellMain>
          </DashboardShellPrimary>
        </DashboardShellInset>
      </DashboardShell>
    </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.

The header stays in view while the content scrolls, in both layouts. From the Sidebar's mobile breakpoint up, the shell is the viewport's height and only DashboardShellMain scrolls. Below it the page scrolls and the header sticks to the top, so Safari can still collapse its toolbars and a tap on the iOS status bar still scrolls to the top. The header's sidebar trigger stays within reach at every width.

"use client";

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

import { Card } from "@/siteplane/components/card";
import {
  DashboardShell,
  DashboardShellHeader,
  DashboardShellInset,
  DashboardShellMain,
  DashboardShellSidebar,
  DashboardShellSidebarHeader,
  DashboardShellSidebarTrigger,
} from "@/siteplane/components/dashboard-shell";
import {
  SidebarBrand,
  SidebarContent,
  SidebarGroup,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarRail,
} 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 (
    // No bounded wrapper: the shell takes the viewport, and the top bar stays
    // in view while the content scrolls, at every width.
    <DashboardShell layout="inset">
      <DashboardShellSidebar>
        <DashboardShellSidebarHeader 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>
          <DashboardShellSidebarTrigger
            aria-label="Collapse sidebar"
            className="group-data-[collapsible=icon]:hidden"
            size="icon-md"
          />
        </DashboardShellSidebarHeader>
        <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 />
      </DashboardShellSidebar>
      <DashboardShellInset>
        <DashboardShellHeader>
          <DashboardShellSidebarTrigger
            aria-label="Toggle sidebar"
            className="md:hidden"
          />
          <h1 className="font-medium text-sm">{active}</h1>
        </DashboardShellHeader>
        <DashboardShellMain className="flex flex-col gap-4">
          <div>
            <h2 className="font-medium text-xl">Activity</h2>
            <p className="mt-1 text-sm text-[var(--sp-rw-fg-tertiary)]">
              Everything that changed in your workspace, newest first.
            </p>
          </div>
          {Array.from({ length: 8 }, (_, index) => (
            <Card className="h-32 shrink-0" key={index} />
          ))}
        </DashboardShellMain>
      </DashboardShellInset>
    </DashboardShell>
  );
}

/** 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>
  );
}

Open the full-width sticky header example.

This example has no bounded wrapper: the shell fills the viewport, as it does in an application. Place DashboardShellHeader directly in DashboardShellInset, before the main area, and the page's content in DashboardShellMain, the part that scrolls from the breakpoint up. The breakpoint is the Sidebar's mobileBreakpoint: md (768 px) by default, or lg (1024 px).

Set stickyHeader={false} for a page that scrolls as a whole. The shell is then at least as tall as the viewport, and the header scrolls away with the page.

Header layouts

The sidebar's first row is DashboardShellSidebarHeader, a band at least as tall as the top bar. With the logo (SidebarBrand) and the 32 px collapse button, it is exactly as tall, so both sit on the top bar's line; in the inset layout it moves down with the framed surface and stays level with the top bar on it. The examples use the inset layout, where the first row holds the collapse button.

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 {
  DashboardShell,
  DashboardShellHeader,
  DashboardShellInset,
  DashboardShellMain,
  DashboardShellSidebar,
  DashboardShellSidebarHeader,
  DashboardShellSidebarTrigger,
} from "@/siteplane/components/dashboard-shell";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/siteplane/components/dropdown-menu";
import {
  SidebarContent,
  SidebarGroup,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarRail,
  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)]">
      <DashboardShell className="h-full min-h-0" layout="inset">
        <DashboardShellSidebar className="h-full">
          {/* The workspace switcher and the collapse button share the first
              row. */}
          <DashboardShellSidebarHeader className="flex-row items-center gap-1">
            <SidebarMenu className="min-w-0 flex-1">
              <WorkspaceSwitcher />
            </SidebarMenu>
            <DashboardShellSidebarTrigger
              aria-label="Collapse sidebar"
              className="group-data-[collapsible=icon]:hidden"
              size="icon-md"
            />
          </DashboardShellSidebarHeader>
          <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 />
        </DashboardShellSidebar>
        <DashboardShellInset>
          <DashboardShellHeader>
            <DashboardShellSidebarTrigger
              aria-label="Toggle sidebar"
              className="md:hidden"
            />
            <h1 className="font-medium text-sm">{active}</h1>
          </DashboardShellHeader>
          <DashboardShellMain>
            <p className="text-sm text-[var(--sp-rw-fg-tertiary)]">
              Your workspace content.
            </p>
          </DashboardShellMain>
        </DashboardShellInset>
      </DashboardShell>
    </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. 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 {
  DashboardShell,
  DashboardShellHeader,
  DashboardShellInset,
  DashboardShellMain,
  DashboardShellSidebar,
  DashboardShellSidebarHeader,
  DashboardShellSidebarTrigger,
} from "@/siteplane/components/dashboard-shell";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/siteplane/components/dropdown-menu";
import {
  SidebarBrand,
  SidebarContent,
  SidebarGroup,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuButtonDescription,
  SidebarMenuButtonText,
  SidebarMenuButtonTitle,
  SidebarMenuItem,
  SidebarRail,
  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)]">
      <DashboardShell className="h-full min-h-0" layout="inset">
        <DashboardShellSidebar className="h-full">
          {/* The first row: the logo and the collapse button, level with the
              top bar and as tall. */}
          <DashboardShellSidebarHeader 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>
            <DashboardShellSidebarTrigger
              aria-label="Collapse sidebar"
              className="group-data-[collapsible=icon]:hidden"
              size="icon-md"
            />
          </DashboardShellSidebarHeader>
          {/* 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 />
        </DashboardShellSidebar>
        <DashboardShellInset>
          <DashboardShellHeader>
            <DashboardShellSidebarTrigger
              aria-label="Toggle sidebar"
              className="md:hidden"
            />
            <h1 className="font-medium text-sm">{active}</h1>
          </DashboardShellHeader>
          <DashboardShellMain>
            <p className="text-sm text-[var(--sp-rw-fg-tertiary)]">
              Your workspace content.
            </p>
          </DashboardShellMain>
        </DashboardShellInset>
      </DashboardShell>
    </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. SidebarBrand sits in a SidebarMenuItem with expandTrigger, so on the collapsed rail hovering or focusing the logo's mark reveals the expand control.

"use client";

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

import {
  DashboardShell,
  DashboardShellHeader,
  DashboardShellInset,
  DashboardShellMain,
  DashboardShellSidebar,
  DashboardShellSidebarHeader,
  DashboardShellSidebarTrigger,
} from "@/siteplane/components/dashboard-shell";
import {
  SidebarBrand,
  SidebarContent,
  SidebarGroup,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarRail,
} 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)]">
      <DashboardShell className="h-full min-h-0" layout="inset">
        <DashboardShellSidebar className="h-full">
          {/* The logo and the collapse button, level with the top bar and as
              tall. With expandTrigger, the logo's mark also expands the
              collapsed rail: hover or focus it to reveal the control. */}
          <DashboardShellSidebarHeader 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>
            <DashboardShellSidebarTrigger
              aria-label="Collapse sidebar"
              className="group-data-[collapsible=icon]:hidden"
              size="icon-md"
            />
          </DashboardShellSidebarHeader>
          <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 />
        </DashboardShellSidebar>
        <DashboardShellInset>
          <DashboardShellHeader>
            <DashboardShellSidebarTrigger
              aria-label="Toggle sidebar"
              className="md:hidden"
            />
            <h1 className="font-medium text-sm">{active}</h1>
          </DashboardShellHeader>
          <DashboardShellMain>
            <p className="text-sm text-[var(--sp-rw-fg-tertiary)]">
              Your workspace content.
            </p>
          </DashboardShellMain>
        </DashboardShellInset>
      </DashboardShell>
    </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>
  );
}

Controlled sidebar state

Pass open and onOpenChange to control desktop collapse; the shell forwards them to SidebarProvider.

"use client";

import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/siteplane/components/card";
import {
  DashboardShell,
  DashboardShellHeader,
  DashboardShellInset,
  DashboardShellMain,
  DashboardShellPrimary,
  DashboardShellSidebar,
  DashboardShellSidebarHeader,
  DashboardShellSidebarTrigger,
} from "@/siteplane/components/dashboard-shell";
import {
  SidebarContent,
  SidebarGroup,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
} from "@/siteplane/components/sidebar";
import { CircleDotIcon, FolderIcon, HomeIcon } from "lucide-react";
import { useState } from "react";

export default function Example() {
  const [active, setActive] = useState("Overview");
  const [open, setOpen] = useState(false);
  return (
    <div className="relative h-[32rem] w-full overflow-hidden [transform:translateZ(0)]">
      <DashboardShell
        className="h-full min-h-0"
        open={open}
        onOpenChange={setOpen}
      >
        <DashboardShellSidebar className="h-full">
          <DashboardShellSidebarHeader>
            <div className="flex items-center gap-1.5 px-2">
              <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>
              <strong className="truncate group-data-[collapsible=icon]:sr-only">
                Acme
              </strong>
            </div>
          </DashboardShellSidebarHeader>
          <SidebarContent>
            <SidebarGroup>
              <SidebarMenu>
                {[
                  { label: "Overview", icon: HomeIcon },
                  { label: "Projects", icon: FolderIcon },
                ].map(({ label, icon: Icon }) => (
                  <SidebarMenuItem key={label}>
                    <SidebarMenuButton
                      active={active === label}
                      tooltip={label}
                      onClick={() => setActive(label)}
                    >
                      <Icon />
                      <span>{label}</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                ))}
              </SidebarMenu>
            </SidebarGroup>
          </SidebarContent>
        </DashboardShellSidebar>
        <DashboardShellInset>
          <DashboardShellHeader>
            <DashboardShellSidebarTrigger />
            <h1 className="font-semibold">{active}</h1>
          </DashboardShellHeader>
          <DashboardShellPrimary>
            <DashboardShellMain>
              <Card>
                <CardHeader>
                  <CardTitle>{active}</CardTitle>
                  <CardDescription>Your workspace at a glance.</CardDescription>
                </CardHeader>
                <CardContent>
                  <p className="text-sm">
                    Three projects are ready for review.
                  </p>
                </CardContent>
              </Card>
            </DashboardShellMain>
          </DashboardShellPrimary>
        </DashboardShellInset>
      </DashboardShell>
    </div>
  );
}

API reference

Component properties
PropTypeDefaultDescription
DashboardShell.layout"inset" | "push""push"Shared sidebar/content layout.
DashboardShell.stickyHeaderbooleantrueKeeps the header in view while the content scrolls. From the Sidebar's mobile breakpoint up, the shell is the viewport's height and only DashboardShellMain scrolls; below it the page scrolls and the header sticks to the top. Set false for a page that scrolls as a whole; the header then scrolls away with it.
DashboardShell defaultOpen / open / onOpenChangeSidebarProvider propstrue / uncontrolled / —Desktop sidebar state. Every other SidebarProvider prop, such as mobileBreakpoint and keyboardShortcut, passes through as well.
DashboardShell.stateCookiestring | false"sidebar_state"Name of the cookie that keeps the desktop sidebar state, or false for no cookie. Read it on the server and pass it to defaultOpen.
DashboardShell style / classNameCSSProperties / stringHeader 3rem, sidebar 15rem, min-h-svhOverride --dashboard-shell-header-height and --sidebar-width through style. With stickyHeader, the shell is 100svh tall from the breakpoint up; a height class, such as the bounded examples' h-full, replaces that height.
DashboardShellSidebar.collapsible"offcanvas" | "icon" | "none""icon"Other props follow Sidebar.
DashboardShellSidebarHeaderSidebarHeader props—The sidebar's first row, a band at least as tall as the top bar (--dashboard-shell-header-height). With SidebarBrand and a collapse button up to 32 px, it is exactly as tall and level with the top bar, also on the framed surface of the inset layout.
DashboardShellInsetSidebarInset propsmainMain content beside navigation.
DashboardShellHeader.dividerbooleantrueHeader divider treatment; other native header props accepted. The header is one line with the main area's 16 px inline inset.
DashboardShellMaindiv props—Padded scroll region with min-h-0 and flex-1.
DashboardShellMain.flushbooleanfalseRemoves the 16 px padding, so a view that lays out its own panes, such as a list beside a detail pane, runs to the edges.
DashboardShellPrimarysection props—Flexible clipped primary region.
DashboardShellSidebarTriggerSidebarTrigger aliasghost, square at the provider size (icon-lg)Desktop/mobile toggle; the same component as SidebarTrigger.

Exported types: DashboardShellLayout, DashboardShellProps, DashboardShellHeaderProps, DashboardShellMainProps. These describe the props above.

Read the installed source.

Usage notes

The sidebar sits flat on the page background in both layouts, with no frame, border or shadow of its own. The main surface is one lighter surface, so the cards on it are white with a visible hairline, and DashboardShellHeader has no fill of its own: it sits on that surface, never on the page background.

DashboardShellMain pads its content by 16 px. A view with panes of its own, such as an inbox with a list beside the open conversation, sets flush instead, so the panes and their dividers meet the edges of the main area and each pane pads its own content.

DashboardShellHeader keeps the same 16 px at both ends in both layouts and at every width, so a title in the header starts on the line where the content starts, and its actions end where the content ends. A DashboardShellSidebarTrigger that leads the header keeps its square target and puts its glyph on that line: its square reaches into the inset by the room beside the glyph, 10 px for the 36 px trigger.

The header is one line. A Breadcrumb in it stays on one row: it takes the room the other header items leave, and its links and the current page end with an ellipsis instead of wrapping. The current page keeps its full text in a title attribute.

In a right-to-left page the shell mirrors: the sidebar, its gap and rail sit at the inline start, on the right, and the header and content keep their insets on the logical sides. See Sidebar.

The shell adds layout, not routing, authentication, or page data. Sidebar mobile behavior, Ctrl/Cmd+B and cookie persistence are inherited from Sidebar; Persist the state shows how to read the cookie back. Use one shell at the application boundary. The bounded wrapper of the other examples is part of their copyable source: it holds the shell at the example's height through h-full on the shell and the sidebar, and the main area scrolls inside it. For a viewport-sized application, remove the wrapper and those height classes (h-full and min-h-0). The shell then fills the viewport and keeps the header in view, as in the sticky header example.

The complete examples include the application's logo, workspace switching, grouped navigation, expandable Library links, item actions, an account menu, icon collapse, a resize rail and a mobile sheet. In the inset layout, the sidebar's first row holds the collapse button and the logo's mark expands the collapsed rail; in the push layout, the header button toggles the sidebar. On mobile, the header button opens the sheet in both layouts. Ctrl/Cmd+B also toggles navigation.

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