Skip to main content
Nexus Design System
GitHub

Sidebar

Sidebar is the app's main navigation panel. It collapses to an icon rail or off screen, and opens as a Sheet on narrow screens. Wrap the app shell in SidebarProvider and put the page content in SidebarInset.

Dashboard
Main content area.
'use client';

import * as React from 'react';

import {
  CalendarBlankIcon,
  GearIcon,
  HouseIcon,
  SquaresFourIcon,
  TrayIcon,
} from '@phosphor-icons/react/ssr';

import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from '@/components/nxds/components/sidebar/sidebar';

function DemoInset({ children }: { children?: React.ReactNode }) {
  return (
    <SidebarInset>
      <header className="nx:flex nx:h-16 nx:items-center nx:gap-2 nx:border-b-default nx:border-border-default nx:px-4">
        <SidebarTrigger />
        <span className="nx:typography-label-default">Dashboard</span>
      </header>
      <div className="nx:min-h-[20rem] nx:p-4 nx:typography-label-default nx:text-muted-foreground">
        {children ?? 'Main content area.'}
      </div>
    </SidebarInset>
  );
}

export default function SidebarAllVariants() {
  return (
    <div className="nx:h-96 nx:w-full nx:overflow-hidden nx:rounded-lg nx:border-default nx:border-border-default">
      <SidebarProvider style={{ minHeight: 'unset', height: '100%' }}>
        <Sidebar collapsible="none">
          <SidebarHeader>
            <SidebarMenu>
              <SidebarMenuItem>
                <SidebarMenuButton size="lg">
                  <SquaresFourIcon />
                  <span>Acme Inc</span>
                </SidebarMenuButton>
              </SidebarMenuItem>
            </SidebarMenu>
          </SidebarHeader>
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Variants</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton isActive>
                      <HouseIcon />
                      <span>Active</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton variant="outline">
                      <TrayIcon />
                      <span>Outline</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton size="sm">
                      <CalendarBlankIcon />
                      <span>Small</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                  <SidebarMenuItem>
                    <SidebarMenuButton asChild>
                      <a href="https://example.com">
                        <GearIcon />
                        <span>As link</span>
                      </a>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
        </Sidebar>
        <DemoInset />
      </SidebarProvider>
    </div>
  );
}

Installation

npx nxds-cli@latest add sidebar

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarSeparator,
  SidebarTrigger,
} from '@/components/nxds/components/sidebar/sidebar';

Composition

SidebarProvider
├── Sidebar
│   ├── SidebarHeader
│   │   ├── SidebarMenu
│   │   │   └── SidebarMenuItem
│   │   │       └── SidebarMenuButton
│   │   └── SidebarInput
│   ├── SidebarContent
│   │   ├── SidebarGroup
│   │   │   ├── SidebarGroupLabel
│   │   │   └── SidebarGroupContent
│   │   │       └── SidebarMenu
│   │   │           └── SidebarMenuItem
│   │   │               ├── SidebarMenuButton
│   │   │               ├── SidebarMenuSub
│   │   │               │   └── SidebarMenuSubItem
│   │   │               │       └── SidebarMenuSubButton
│   │   │               └── SidebarMenuBadge
│   │   ├── SidebarSeparator
│   │   └── SidebarMenu
│   │       └── SidebarMenuItem
│   │           ├── SidebarMenuButton
│   │           └── SidebarMenuBadge
│   ├── SidebarRail
│   └── SidebarFooter
│       └── SidebarMenu
│           └── SidebarMenuItem
│               └── SidebarMenuButton
└── SidebarInset
    └── SidebarTrigger

Examples

With submenu

Nest pages under a menu item with SidebarMenuSub.

Dashboard
Main content area.
'use client';

import * as React from 'react';

import {
  CaretRightIcon,
  FolderIcon,
  TrayIcon,
} from '@phosphor-icons/react/ssr';

import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarSeparator,
  SidebarTrigger,
} from '@/components/nxds/components/sidebar/sidebar';

function DemoInset({ children }: { children?: React.ReactNode }) {
  return (
    <SidebarInset>
      <header className="nx:flex nx:h-16 nx:items-center nx:gap-2 nx:border-b-default nx:border-border-default nx:px-4">
        <SidebarTrigger />
        <span className="nx:typography-label-default">Dashboard</span>
      </header>
      <div className="nx:min-h-[20rem] nx:p-4 nx:typography-label-default nx:text-muted-foreground">
        {children ?? 'Main content area.'}
      </div>
    </SidebarInset>
  );
}

export default function SidebarWithSubmenu() {
  return (
    <div className="nx:h-96 nx:w-full nx:overflow-hidden nx:rounded-lg nx:border-default nx:border-border-default">
      <SidebarProvider style={{ minHeight: 'unset', height: '100%' }}>
        <Sidebar collapsible="none">
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Projects</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton isActive>
                      <FolderIcon />
                      <span>Design System</span>
                      <CaretRightIcon className="nx:ml-auto" />
                    </SidebarMenuButton>
                    <SidebarMenuSub>
                      <SidebarMenuSubItem>
                        <SidebarMenuSubButton href="#tokens" isActive>
                          Tokens
                        </SidebarMenuSubButton>
                      </SidebarMenuSubItem>
                      <SidebarMenuSubItem>
                        <SidebarMenuSubButton href="#components">
                          Components
                        </SidebarMenuSubButton>
                      </SidebarMenuSubItem>
                      <SidebarMenuSubItem>
                        <SidebarMenuSubButton href="#documentation">
                          Documentation
                        </SidebarMenuSubButton>
                      </SidebarMenuSubItem>
                    </SidebarMenuSub>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
            <SidebarSeparator />
            <SidebarGroup>
              <SidebarGroupContent>
                <SidebarMenu>
                  <SidebarMenuItem>
                    <SidebarMenuButton>
                      <TrayIcon />
                      <span>Inbox</span>
                    </SidebarMenuButton>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
          <SidebarRail />
        </Sidebar>
        <DemoInset />
      </SidebarProvider>
    </div>
  );
}

Compact

Set size="sm" on every row for a denser sidebar with 12px text.

Dashboard
Compact rows use the small size (12px body-small) for denser, information-heavy sidebars.
'use client';

import * as React from 'react';

import {
  CalendarBlankIcon,
  CaretRightIcon,
  FolderIcon,
  GearIcon,
  HouseIcon,
  MagnifyingGlassIcon,
  TrayIcon,
  UserIcon,
} from '@phosphor-icons/react/ssr';

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
} from '@/components/nxds/components/sidebar/sidebar';

const NAV_ITEMS = [
  { title: 'Home', icon: HouseIcon },
  { title: 'Inbox', icon: TrayIcon, badge: '4' },
  { title: 'Calendar', icon: CalendarBlankIcon },
  { title: 'Search', icon: MagnifyingGlassIcon },
  { title: 'Settings', icon: GearIcon },
];

function DemoInset({ children }: { children?: React.ReactNode }) {
  return (
    <SidebarInset>
      <header className="nx:flex nx:h-16 nx:items-center nx:gap-2 nx:border-b-default nx:border-border-default nx:px-4">
        <SidebarTrigger />
        <span className="nx:typography-label-default">Dashboard</span>
      </header>
      <div className="nx:min-h-[20rem] nx:p-4 nx:typography-label-default nx:text-muted-foreground">
        {children ?? 'Main content area.'}
      </div>
    </SidebarInset>
  );
}

export default function SidebarCompact() {
  return (
    <div className="nx:h-96 nx:w-full nx:overflow-hidden nx:rounded-lg nx:border-default nx:border-border-default">
      <SidebarProvider style={{ minHeight: 'unset', height: '100%' }}>
        <Sidebar collapsible="none">
          <SidebarHeader>
            <SidebarInput aria-label="Search" placeholder="Search…" />
          </SidebarHeader>
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Platform</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  {NAV_ITEMS.map((item) => {
                    const Icon = item.icon;
                    return (
                      <SidebarMenuItem key={item.title}>
                        <SidebarMenuButton
                          size="sm"
                          tooltip={item.title}
                          isActive={item.title === 'Home'}
                        >
                          <Icon />
                          <span>{item.title}</span>
                        </SidebarMenuButton>
                        {item.badge && (
                          <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
                        )}
                      </SidebarMenuItem>
                    );
                  })}
                  <SidebarMenuItem>
                    <SidebarMenuButton size="sm" tooltip="Projects">
                      <FolderIcon />
                      <span>Projects</span>
                      <CaretRightIcon className="nx:ml-auto" />
                    </SidebarMenuButton>
                    <SidebarMenuSub>
                      <SidebarMenuSubItem>
                        <SidebarMenuSubButton href="#overview" size="sm">
                          Overview
                        </SidebarMenuSubButton>
                      </SidebarMenuSubItem>
                      <SidebarMenuSubItem>
                        <SidebarMenuSubButton href="#mobile" size="sm" isActive>
                          Mobile App
                        </SidebarMenuSubButton>
                      </SidebarMenuSubItem>
                    </SidebarMenuSub>
                  </SidebarMenuItem>
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
          <SidebarFooter>
            <SidebarMenu>
              <SidebarMenuItem>
                <SidebarMenuButton size="sm" tooltip="Account">
                  <UserIcon />
                  <span>Account</span>
                </SidebarMenuButton>
              </SidebarMenuItem>
            </SidebarMenu>
          </SidebarFooter>
          <SidebarRail />
        </Sidebar>
        <DemoInset>
          Compact rows use the small size (12px body-small) for denser,
          information-heavy sidebars.
        </DemoInset>
      </SidebarProvider>
    </div>
  );
}

Flat

Skip the group parts and put nx:p-2 on SidebarContent for a flat list of links.

Dashboard
Flat structure — no SidebarGroup/-Label/-Content, just a SidebarMenu of buttons.
'use client';

import * as React from 'react';

import {
  CalendarBlankIcon,
  GearIcon,
  HouseIcon,
  MagnifyingGlassIcon,
  TrayIcon,
} from '@phosphor-icons/react/ssr';

import {
  Sidebar,
  SidebarContent,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
} from '@/components/nxds/components/sidebar/sidebar';

const NAV_ITEMS = [
  { title: 'Home', icon: HouseIcon },
  { title: 'Inbox', icon: TrayIcon, badge: '4' },
  { title: 'Calendar', icon: CalendarBlankIcon },
  { title: 'Search', icon: MagnifyingGlassIcon },
  { title: 'Settings', icon: GearIcon },
];

function DemoInset({ children }: { children?: React.ReactNode }) {
  return (
    <SidebarInset>
      <header className="nx:flex nx:h-16 nx:items-center nx:gap-2 nx:border-b-default nx:border-border-default nx:px-4">
        <SidebarTrigger />
        <span className="nx:typography-label-default">Dashboard</span>
      </header>
      <div className="nx:min-h-[20rem] nx:p-4 nx:typography-label-default nx:text-muted-foreground">
        {children ?? 'Main content area.'}
      </div>
    </SidebarInset>
  );
}

export default function SidebarFlat() {
  return (
    <div className="nx:h-96 nx:w-full nx:overflow-hidden nx:rounded-lg nx:border-default nx:border-border-default">
      <SidebarProvider style={{ minHeight: 'unset', height: '100%' }}>
        <Sidebar collapsible="none">
          <SidebarContent className="nx:p-2">
            <SidebarMenu>
              {NAV_ITEMS.map((item) => {
                const Icon = item.icon;
                return (
                  <SidebarMenuItem key={item.title}>
                    <SidebarMenuButton
                      tooltip={item.title}
                      isActive={item.title === 'Home'}
                    >
                      <Icon />
                      <span>{item.title}</span>
                    </SidebarMenuButton>
                    {item.badge && (
                      <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
                    )}
                  </SidebarMenuItem>
                );
              })}
            </SidebarMenu>
          </SidebarContent>
          <SidebarRail />
        </Sidebar>
        <DemoInset>
          Flat structure — no SidebarGroup/-Label/-Content, just a SidebarMenu
          of buttons.
        </DemoInset>
      </SidebarProvider>
    </div>
  );
}

Props

Sidebar

Sidebar props
PropTypeDefaultDescription
collapsible"icon" | "none" | "offcanvas""offcanvas"

How it collapses: slides off screen, shrinks to an icon rail, or never.

side"left" | "right""left"

Edge the sidebar docks to.

variant"inset" | "floating" | "sidebar""sidebar"

Visual style. inset pairs with SidebarInset to show the main region as a card.

SidebarContent

SidebarContent adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarFooter adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarGroup

SidebarGroup adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarGroupAction

SidebarGroupAction props
PropTypeDefaultDescription
asChildbooleanfalse

Renders the child element instead.

SidebarGroupContent

SidebarGroupContent adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarGroupLabel

SidebarGroupLabel props
PropTypeDefaultDescription
asChildbooleanfalse

Renders the child element instead, such as a heading.

SidebarHeader

SidebarHeader adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarInput

SidebarInput props
PropTypeDefaultDescription
size"default" | "sm" | "lg" | null"default"No description
variant"ghost" | "borderless" | "bordered" | null"bordered"No description

SidebarInset

SidebarInset adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarMenu

SidebarMenu adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarMenuAction

SidebarMenuAction props
PropTypeDefaultDescription
asChildbooleanfalse

Renders the child element instead.

showOnHoverbooleanfalse

Shows the action only on hover or focus of the menu item.

SidebarMenuBadge

SidebarMenuBadge adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarMenuButton

SidebarMenuButton props
PropTypeDefaultDescription
asChildbooleanfalse

Renders the child element instead, such as a router link.

isActivebooleanfalse

Marks the item as the current page.

size"default" | "sm" | "lg" | null"default"No description
tooltipstring | TooltipContentPropsNo default documented

Tooltip shown only on the icon rail. A string or TooltipContent props.

variant"default" | "outline" | null"default"No description

SidebarMenuItem

SidebarMenuItem adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarMenuSkeleton

SidebarMenuSkeleton props
PropTypeDefaultDescription
showIconbooleanfalse

Adds an icon-sized block before the text bar.

SidebarMenuSub

SidebarMenuSub adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarMenuSubButton

SidebarMenuSubButton props
PropTypeDefaultDescription
asChildbooleanfalse

Renders the child element instead, such as a router link.

isActivebooleanfalse

Marks the sub-item as the current page.

size"sm" | "md""md"

Text size of the sub-item.

SidebarMenuSubItem

SidebarMenuSubItem adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarProvider

SidebarProvider props
PropTypeDefaultDescription
defaultOpenbooleantrue

Opens the sidebar on first render when uncontrolled. The open state is saved to a sidebar_state cookie. Read that cookie on the server and pass it here to restore it.

onOpenChange((open: boolean) => void)No default documented

Called when the open state changes.

openbooleanNo default documented

Controlled open state.

SidebarRail

SidebarRail adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarSeparator

SidebarSeparator adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarTrigger

SidebarTrigger props
PropTypeDefaultDescription
asChildbooleanfalse

Renders the child, such as an <a>, with button styles. The loading spinner and icon slots then don't render.

endIconReactNodeNo default documented

Decorative icon after the label. Use endIcon or startIcon, not both.

loadingbooleanfalse

Disables the button and shows a spinner in place of its content.

size"default" | "sm" | "lg" | "icon-sm" | "icon" | "icon-lg" | null"icon"No description
startIconReactNodeNo default documented

Decorative icon before the label. Use startIcon or endIcon, not both.

variant"link" | "default" | "error" | "destructive" | "outline" | "dashed" | "secondary" | "ghost" | null"ghost"No description

Sidebar is the app's main navigation panel. It collapses to an icon rail or off screen, and opens as a Sheet on narrow screens. Wrap the app shell in SidebarProvider and put the page content in SidebarInset.

<script setup lang="ts">
import {
  PhCalendarBlank,
  PhGear,
  PhHouse,
  PhSquaresFour,
  PhTray,
} from '@phosphor-icons/vue';

import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarTrigger,
} from '@/components/sidebar';
</script>

<template>
  <div
    class="nx:h-96 nx:w-full nx:overflow-hidden nx:rounded-lg nx:border-default nx:border-border-default"
  >
    <SidebarProvider :style="{ minHeight: 'unset', height: '100%' }">
      <Sidebar collapsible="none">
        <SidebarHeader>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton size="lg">
                <PhSquaresFour />
                <span>Acme Inc</span>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Variants</SidebarGroupLabel>
            <SidebarGroupContent>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuButton is-active>
                    <PhHouse />
                    <span>Active</span>
                  </SidebarMenuButton>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuButton variant="outline">
                    <PhTray />
                    <span>Outline</span>
                  </SidebarMenuButton>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuButton size="sm">
                    <PhCalendarBlank />
                    <span>Small</span>
                  </SidebarMenuButton>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuButton as-child>
                    <a href="https://example.com">
                      <PhGear />
                      <span>As link</span>
                    </a>
                  </SidebarMenuButton>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
      </Sidebar>
      <SidebarInset>
        <header
          class="nx:flex nx:h-16 nx:items-center nx:gap-2 nx:border-b-default nx:border-border-default nx:px-4"
        >
          <SidebarTrigger />
          <span class="nx:typography-label-default">Dashboard</span>
        </header>
        <div
          class="nx:min-h-[20rem] nx:p-4 nx:typography-label-default nx:text-muted-foreground"
        >
          Main content area.
        </div>
      </SidebarInset>
    </SidebarProvider>
  </div>
</template>

Installation

  1. Install these components first:

    Button, ButtonGroup, Input, Separator, Sheet, Skeleton, Spinner, Tooltip.

  2. Copy and paste the following code into your project.

    components/sidebar/Sidebar.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    import {
      Sheet,
      SheetContent,
      SheetDescription,
      SheetHeader,
      SheetTitle,
    } from '../sheet';
    
    import { useSidebar } from '.';
    
    const SIDEBAR_WIDTH_NARROW = '18rem';
    
    export interface SidebarProps {
      /**
       * Edge the sidebar docks to.
       */
      side?: 'left' | 'right';
      /**
       * Visual style. `inset` pairs with `SidebarInset` to show the main region as a
       * card.
       */
      variant?: 'sidebar' | 'floating' | 'inset';
      /**
       * How it collapses: slides off screen, shrinks to an icon rail, or never.
       */
      collapsible?: 'offcanvas' | 'icon' | 'none';
      class?: HTMLAttributes['class'];
    }
    
    defineOptions({ inheritAttrs: false });
    
    const props = withDefaults(defineProps<SidebarProps>(), {
      side: 'left',
      variant: 'sidebar',
      collapsible: 'offcanvas',
    });
    
    const { isNarrow, state, openNarrow, setOpenNarrow } = useSidebar();
    
    // The drawer opens from state, not a SheetTrigger, so it has no trigger to
    // return focus to. Return it to whatever had focus when it opened.
    let returnFocus: HTMLElement | null = null;
    
    function rememberFocus() {
      const opener = document.activeElement;
      returnFocus = opener instanceof HTMLElement ? opener : null;
    }
    
    function restoreFocus(event: Event) {
      if (!returnFocus?.isConnected) return;
      event.preventDefault();
      returnFocus.focus();
    }
    </script>
    
    <template>
      <div
        v-if="collapsible === 'none'"
        data-slot="sidebar"
        :class="
          cn(
            'nx:flex nx:h-full nx:w-(--sidebar-width) nx:flex-col nx:bg-nav-background nx:text-nav-foreground',
            props.class
          )
        "
        v-bind="$attrs"
      >
        <slot />
      </div>
      <Sheet v-else-if="isNarrow" :open="openNarrow" @update:open="setOpenNarrow">
        <SheetContent
          data-slot="sidebar"
          data-narrow="true"
          :side="side"
          :class="
            cn(
              'nx:inset-y-2 nx:h-auto nx:w-(--sidebar-width) nx:max-w-[calc(100vw-1rem)] nx:rounded-lg nx:border-default nx:border-nav-border nx:bg-nav-background nx:p-0 nx:text-nav-foreground',
              side === 'left'
                ? 'nx:left-2 nx:data-[state=closed]:-translate-x-[calc(100%+0.5rem)]'
                : 'nx:right-2 nx:data-[state=closed]:translate-x-[calc(100%+0.5rem)]',
              props.class
            )
          "
          :style="{ '--sidebar-width': SIDEBAR_WIDTH_NARROW }"
          v-bind="$attrs"
          @open-auto-focus="rememberFocus"
          @close-auto-focus="restoreFocus"
        >
          <SheetHeader class="nx:sr-only">
            <SheetTitle>Sidebar</SheetTitle>
            <SheetDescription>Displays the narrow-tier sidebar.</SheetDescription>
          </SheetHeader>
          <div class="nx:flex nx:h-full nx:w-full nx:flex-col">
            <slot />
          </div>
        </SheetContent>
      </Sheet>
      <div
        v-else
        class="nx:group nx:peer nx:hidden nx:text-nav-foreground nx:md:block"
        :data-state="state"
        :data-collapsible="state === 'collapsed' ? collapsible : ''"
        :data-variant="variant"
        :data-side="side"
        data-slot="sidebar"
      >
        <div
          data-slot="sidebar-gap"
          :class="
            cn(
              'nx:relative nx:w-(--sidebar-width) nx:bg-transparent nx:transition-[width] nx:duration-default nx:ease-linear',
              'nx:group-data-[collapsible=offcanvas]:w-0',
              'nx:group-data-[side=right]:rotate-180',
              variant === 'floating' || variant === 'inset'
                ? 'nx:group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+1rem)]'
                : 'nx:group-data-[collapsible=icon]:w-(--sidebar-width-icon)'
            )
          "
        />
        <div
          data-slot="sidebar-container"
          :class="
            cn(
              'nx:fixed nx:inset-y-0 nx:z-sticky nx:hidden nx:h-svh nx:w-(--sidebar-width) nx:transition-[left,right,width] nx:duration-default nx:ease-linear nx:md:flex',
              side === 'left'
                ? 'nx:left-0 nx:group-data-[collapsible=offcanvas]:-left-(--sidebar-width)'
                : 'nx:right-0 nx:group-data-[collapsible=offcanvas]:-right-(--sidebar-width)',
              (variant === 'floating' || variant === 'inset') && 'nx:p-2',
              variant === 'floating' || variant === 'inset'
                ? 'nx:group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+1rem+2px)]'
                : 'nx:group-data-[collapsible=icon]:w-(--sidebar-width-icon) nx:group-data-[side=left]:border-r-default nx:group-data-[side=right]:border-l-default nx:border-nav-border',
              props.class
            )
          "
          v-bind="$attrs"
        >
          <div
            data-slot="sidebar-inner"
            class="nx:flex nx:h-full nx:w-full nx:flex-col nx:bg-nav-background nx:group-data-[variant=floating]:rounded-lg nx:group-data-[variant=floating]:border-default nx:group-data-[variant=floating]:border-nav-border nx:group-data-[variant=floating]:shadow-sm"
          >
            <slot />
          </div>
        </div>
      </div>
    </template>
    
    components/sidebar/SidebarContent.vue
    <script setup lang="ts">
    import { type HTMLAttributes, useAttrs } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarContentProps {
      class?: HTMLAttributes['class'];
    }
    
    defineOptions({ inheritAttrs: false });
    
    const props = defineProps<SidebarContentProps>();
    
    const attrs = useAttrs();
    
    // A labelledby reference, when given, names the landmark, so the default
    // label is left off rather than emitted beside it.
    function ariaLabel() {
      if (attrs['aria-labelledby']) return undefined;
      return (attrs['aria-label'] as string | undefined) ?? 'Sidebar';
    }
    </script>
    
    <template>
      <nav
        v-bind="{
          'data-slot': 'sidebar-content',
          ...attrs,
          'aria-label': ariaLabel(),
        }"
        :class="
          cn(
            'nx:flex nx:min-h-0 nx:flex-1 nx:flex-col nx:gap-2 nx:overflow-auto nx:group-data-[collapsible=icon]:overflow-hidden',
            props.class
          )
        "
      >
        <slot />
      </nav>
    </template>
    
    components/sidebar/SidebarFooter.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarFooterProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<SidebarFooterProps>();
    </script>
    
    <template>
      <div
        data-slot="sidebar-footer"
        :class="cn('nx:flex nx:flex-col nx:gap-2 nx:p-2', props.class)"
      >
        <slot />
      </div>
    </template>
    
    components/sidebar/SidebarGroup.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarGroupProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<SidebarGroupProps>();
    </script>
    
    <template>
      <div
        data-slot="sidebar-group"
        :class="
          cn(
            'nx:relative nx:flex nx:w-full nx:min-w-0 nx:flex-col nx:p-2',
            props.class
          )
        "
      >
        <slot />
      </div>
    </template>
    
    components/sidebar/SidebarGroupAction.vue
    <script setup lang="ts">
    import { Primitive, type PrimitiveProps } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarGroupActionProps extends PrimitiveProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<SidebarGroupActionProps>(), {
      as: 'button',
    });
    </script>
    
    <template>
      <Primitive
        data-slot="sidebar-group-action"
        :as="as"
        :as-child="asChild"
        :class="
          cn(
            'nx:absolute nx:top-3.5 nx:right-3 nx:flex nx:aspect-square nx:w-5 nx:items-center nx:justify-center nx:rounded-md nx:border-0 nx:bg-transparent nx:p-0 nx:text-nav-foreground nx:hover:bg-nav-item-hover nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default nx:[&>svg]:size-4 nx:[&>svg]:shrink-0',
            'nx:group-data-[collapsible=icon]:hidden',
            props.class
          )
        "
      >
        <slot />
      </Primitive>
    </template>
    
    components/sidebar/SidebarGroupContent.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarGroupContentProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<SidebarGroupContentProps>();
    </script>
    
    <template>
      <div
        data-slot="sidebar-group-content"
        :class="cn('nx:w-full nx:typography-body-default', props.class)"
      >
        <slot />
      </div>
    </template>
    
    components/sidebar/SidebarGroupLabel.vue
    <script setup lang="ts">
    import { Primitive, type PrimitiveProps } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarGroupLabelProps extends PrimitiveProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<SidebarGroupLabelProps>(), {
      as: 'div',
    });
    </script>
    
    <template>
      <Primitive
        data-slot="sidebar-group-label"
        :as="as"
        :as-child="asChild"
        :class="
          cn(
            'nx:flex nx:h-8 nx:shrink-0 nx:items-center nx:rounded-md nx:px-2 nx:typography-label-small nx:text-nav-muted-foreground nx:transition-[margin,opacity] nx:duration-default nx:ease-linear nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default nx:[&>svg]:size-4 nx:[&>svg]:shrink-0',
            'nx:group-data-[collapsible=icon]:-mt-8 nx:group-data-[collapsible=icon]:opacity-0',
            props.class
          )
        "
      >
        <slot />
      </Primitive>
    </template>
    
    components/sidebar/SidebarHeader.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarHeaderProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<SidebarHeaderProps>();
    </script>
    
    <template>
      <div
        data-slot="sidebar-header"
        :class="cn('nx:flex nx:flex-col nx:gap-2 nx:p-2', props.class)"
      >
        <slot />
      </div>
    </template>
    
    components/sidebar/SidebarInput.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    import { Input, type InputVariants } from '../input';
    
    // `v-model` falls through to Input.
    export interface SidebarInputProps {
      size?: InputVariants['size'];
      variant?: InputVariants['variant'];
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<SidebarInputProps>(), {
      size: 'default',
      variant: 'bordered',
    });
    </script>
    
    <template>
      <Input
        data-slot="sidebar-input"
        :size="size"
        :variant="variant"
        :class="
          cn(
            'nx:h-8 nx:w-full nx:bg-background nx:autofill-bg-background',
            props.class
          )
        "
      />
    </template>
    
    components/sidebar/SidebarInset.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarInsetProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<SidebarInsetProps>();
    </script>
    
    <template>
      <main
        data-slot="sidebar-inset"
        :class="
          cn(
            'nx:relative nx:flex nx:w-full nx:min-w-0 nx:flex-1 nx:flex-col nx:bg-background',
            'nx:md:peer-data-[variant=inset]:m-2 nx:md:peer-data-[variant=inset]:ml-0 nx:md:peer-data-[variant=inset]:rounded-xl nx:md:peer-data-[variant=inset]:shadow-sm nx:md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2',
            props.class
          )
        "
      >
        <slot />
      </main>
    </template>
    
    components/sidebar/SidebarMenu.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarMenuProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<SidebarMenuProps>();
    </script>
    
    <template>
      <ul
        data-slot="sidebar-menu"
        :class="
          cn(
            'nx:m-0 nx:flex nx:w-full nx:min-w-0 nx:list-none nx:flex-col nx:gap-px nx:p-0',
            props.class
          )
        "
      >
        <slot />
      </ul>
    </template>
    
    components/sidebar/SidebarMenuAction.vue
    <script setup lang="ts">
    import { Primitive, type PrimitiveProps } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarMenuActionProps extends PrimitiveProps {
      /**
       * Shows the action only on hover or focus of the menu item.
       */
      showOnHover?: boolean;
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<SidebarMenuActionProps>(), {
      as: 'button',
      showOnHover: false,
    });
    </script>
    
    <template>
      <Primitive
        data-slot="sidebar-menu-action"
        :as="as"
        :as-child="asChild"
        :class="
          cn(
            'nx:absolute nx:top-1.5 nx:right-1 nx:flex nx:aspect-square nx:w-5 nx:items-center nx:justify-center nx:rounded-md nx:border-0 nx:bg-transparent nx:p-0 nx:text-nav-foreground nx:hover:bg-nav-item-hover nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default nx:[&>svg]:size-4 nx:[&>svg]:shrink-0',
            'nx:peer-data-[size=sm]/menu-button:top-1',
            'nx:peer-data-[size=default]/menu-button:top-1.5',
            'nx:peer-data-[size=lg]/menu-button:top-2.5',
            'nx:group-data-[collapsible=icon]:hidden',
            showOnHover &&
              'nx:group-focus-within/menu-item:opacity-100 nx:group-hover/menu-item:opacity-100 nx:data-[state=open]:opacity-100 nx:md:opacity-0',
            props.class
          )
        "
      >
        <slot />
      </Primitive>
    </template>
    
    components/sidebar/SidebarMenuBadge.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarMenuBadgeProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<SidebarMenuBadgeProps>();
    </script>
    
    <template>
      <div
        data-slot="sidebar-menu-badge"
        :class="
          cn(
            'nx:pointer-events-none nx:absolute nx:right-1 nx:flex nx:h-5 nx:min-w-5 nx:items-center nx:justify-center nx:rounded-md nx:px-1 nx:typography-label-small nx:text-nav-foreground nx:tabular-nums nx:select-none',
            'nx:peer-data-[size=sm]/menu-button:top-1',
            'nx:peer-data-[size=default]/menu-button:top-1.5',
            'nx:peer-data-[size=lg]/menu-button:top-2.5',
            'nx:group-data-[collapsible=icon]:hidden',
            props.class
          )
        "
      >
        <slot />
      </div>
    </template>
    
    components/sidebar/SidebarMenuButton.vue
    <script setup lang="ts">
    import { Primitive, type PrimitiveProps } from 'reka-ui';
    import { computed, type HTMLAttributes, ref, type VNode, watch } from 'vue';
    
    import { cn } from '../../lib/utils';
    import {
      Tooltip,
      TooltipContent,
      type TooltipContentProps,
      TooltipTrigger,
    } from '../tooltip';
    
    import {
      type SidebarMenuButtonVariants,
      sidebarMenuButtonVariants,
      useSidebar,
    } from '.';
    
    export interface SidebarMenuButtonProps extends PrimitiveProps {
      /**
       * Marks the item as the current page.
       */
      isActive?: boolean;
      variant?: SidebarMenuButtonVariants['variant'];
      size?: SidebarMenuButtonVariants['size'];
      /**
       * Tooltip shown only on the icon rail. A string or `TooltipContent` props.
       */
      tooltip?: string | TooltipContentProps;
      class?: HTMLAttributes['class'];
    }
    
    defineOptions({ inheritAttrs: false });
    
    const props = withDefaults(defineProps<SidebarMenuButtonProps>(), {
      as: 'button',
      isActive: false,
      variant: 'default',
      size: 'default',
    });
    
    // Unless `tooltip` is a string, the tooltip text goes in this slot.
    defineSlots<{
      default?: () => VNode[];
      tooltip?: () => VNode[];
    }>();
    
    const { isNarrow, state } = useSidebar();
    const isRail = computed(() => state.value === 'collapsed' && !isNarrow.value);
    const open = ref(false);
    // Reka never reports the close once the button leaves the rail.
    watch(isRail, (rail) => {
      if (!rail) open.value = false;
    });
    
    const button = computed(() => ({
      as: props.as,
      asChild: props.asChild,
      'data-slot': 'sidebar-menu-button',
      'data-variant': props.variant,
      'data-size': props.size,
      'data-active': props.isActive,
      'aria-current': props.isActive ? ('page' as const) : undefined,
      class: cn(
        sidebarMenuButtonVariants({ variant: props.variant, size: props.size }),
        props.class
      ),
    }));
    </script>
    
    <template>
      <Tooltip
        v-if="tooltip || $slots.tooltip"
        :open="open"
        @update:open="open = $event && isRail"
      >
        <TooltipTrigger as-child>
          <Primitive v-bind="{ ...button, ...$attrs }">
            <slot />
          </Primitive>
        </TooltipTrigger>
        <TooltipContent
          side="right"
          align="center"
          v-bind="typeof tooltip === 'object' ? tooltip : {}"
        >
          <template v-if="typeof tooltip === 'string'">{{ tooltip }}</template>
          <slot v-else name="tooltip" />
        </TooltipContent>
      </Tooltip>
      <Primitive v-else v-bind="{ ...button, ...$attrs }">
        <slot />
      </Primitive>
    </template>
    
    components/sidebar/SidebarMenuItem.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarMenuItemProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<SidebarMenuItemProps>();
    </script>
    
    <template>
      <li
        data-slot="sidebar-menu-item"
        :class="cn('nx:group/menu-item nx:relative', props.class)"
      >
        <slot />
      </li>
    </template>
    
    components/sidebar/SidebarMenuSkeleton.vue
    <script setup lang="ts">
    import { type HTMLAttributes, useId } from 'vue';
    
    import { cn } from '../../lib/utils';
    import { Skeleton } from '../skeleton';
    
    export interface SidebarMenuSkeletonProps {
      /**
       * Adds an icon-sized block before the text bar.
       */
      showIcon?: boolean;
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<SidebarMenuSkeletonProps>(), {
      showIcon: false,
    });
    
    // Vary the text-bar width (50–90%) so a stack of skeletons looks organic.
    // Seed from the SSR-stable useId() so server and client agree.
    const seed = [...useId()].reduce((sum, char) => sum + char.charCodeAt(0), 0);
    const width = `${50 + (seed % 41)}%`;
    </script>
    
    <template>
      <div
        data-slot="sidebar-menu-skeleton"
        :class="
          cn(
            'nx:flex nx:h-8 nx:items-center nx:gap-2 nx:rounded-md nx:px-2',
            props.class
          )
        "
      >
        <Skeleton
          v-if="showIcon"
          data-slot="sidebar-menu-skeleton-icon"
          class="nx:size-4 nx:rounded-md"
        />
        <Skeleton
          data-slot="sidebar-menu-skeleton-text"
          class="nx:h-4 nx:max-w-(--skeleton-width) nx:flex-1"
          :style="{ '--skeleton-width': width }"
        />
      </div>
    </template>
    
    components/sidebar/SidebarMenuSub.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarMenuSubProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<SidebarMenuSubProps>();
    </script>
    
    <template>
      <ul
        data-slot="sidebar-menu-sub"
        :class="
          cn(
            'nx:my-0 nx:mr-0 nx:ml-3.5 nx:flex nx:min-w-0 nx:list-none nx:translate-x-px nx:flex-col nx:gap-px nx:border-l-default nx:border-nav-border nx:pl-2.5 nx:py-0.5',
            'nx:group-data-[collapsible=icon]:hidden',
            props.class
          )
        "
      >
        <slot />
      </ul>
    </template>
    
    components/sidebar/SidebarMenuSubButton.vue
    <script setup lang="ts">
    import { Primitive, type PrimitiveProps } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarMenuSubButtonProps extends PrimitiveProps {
      /**
       * Text size of the sub-item.
       */
      size?: 'sm' | 'md';
      /**
       * Marks the sub-item as the current page.
       */
      isActive?: boolean;
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<SidebarMenuSubButtonProps>(), {
      as: 'a',
      size: 'md',
      isActive: false,
    });
    </script>
    
    <template>
      <Primitive
        data-slot="sidebar-menu-sub-button"
        :as="as"
        :as-child="asChild"
        :data-size="size"
        :data-active="isActive"
        :aria-current="isActive ? 'page' : undefined"
        :class="
          cn(
            'nx:flex nx:h-7 nx:min-w-0 nx:-translate-x-px nx:items-center nx:gap-2 nx:overflow-hidden nx:rounded-md nx:px-2 nx:text-nav-muted-foreground nx:no-underline nx:hover:bg-nav-item-hover nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default nx:active:bg-nav-item-active nx:disabled:pointer-events-none nx:disabled:text-disabled-foreground nx:aria-disabled:pointer-events-none nx:aria-disabled:text-disabled-foreground nx:[&>span:last-child]:truncate nx:[&>svg]:size-4 nx:[&>svg]:shrink-0',
            'nx:data-[active=true]:bg-nav-item-active nx:data-[active=true]:text-nav-foreground',
            size === 'sm' && 'nx:typography-body-small',
            size === 'md' && 'nx:typography-body-default',
            'nx:group-data-[collapsible=icon]:hidden',
            props.class
          )
        "
      >
        <slot />
      </Primitive>
    </template>
    
    components/sidebar/SidebarMenuSubItem.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface SidebarMenuSubItemProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<SidebarMenuSubItemProps>();
    </script>
    
    <template>
      <li
        data-slot="sidebar-menu-sub-item"
        :class="cn('nx:group/menu-sub-item nx:relative', props.class)"
      >
        <slot />
      </li>
    </template>
    
    components/sidebar/SidebarProvider.vue
    <script setup lang="ts">
    import { useEventListener, useMediaQuery } from '@vueuse/core';
    import { computed, type HTMLAttributes, provide, ref } from 'vue';
    
    import { cn } from '../../lib/utils';
    import TooltipProvider from '../tooltip/TooltipProvider.vue';
    
    import { SidebarKey } from './sidebar-context';
    
    const SIDEBAR_COOKIE_NAME = 'sidebar_state';
    const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
    const SIDEBAR_WIDTH = '16rem';
    const SIDEBAR_WIDTH_ICON = '3rem';
    const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
    
    /** Whether the event target is a text-entry element (input, textarea, or contenteditable). */
    function isEditableTarget(target: EventTarget | null) {
      if (!(target instanceof HTMLElement)) return false;
      return (
        target.isContentEditable ||
        target.tagName === 'INPUT' ||
        target.tagName === 'TEXTAREA'
      );
    }
    
    export interface SidebarProviderProps {
      /**
       * Opens the sidebar on first render when uncontrolled. The open state is saved
       * to a `sidebar_state` cookie. Read that cookie on the server and pass it here
       * to restore it.
       */
      defaultOpen?: boolean;
      /**
       * Controlled open state.
       */
      open?: boolean;
      class?: HTMLAttributes['class'];
    }
    
    defineOptions({ inheritAttrs: false });
    
    const props = withDefaults(defineProps<SidebarProviderProps>(), {
      defaultOpen: true,
      open: undefined,
    });
    const emit = defineEmits<{
      /**
       * Called when the open state changes.
       */
      'open-change': [open: boolean];
    }>();
    
    // The Narrow tier: viewports below `md` (48rem), where the sidebar is a Sheet.
    const isNarrow = useMediaQuery('(max-width: 47.99rem)');
    const openNarrow = ref(false);
    
    // Internal open state; the `open` prop takes over when controlled.
    const uncontrolledOpen = ref(props.defaultOpen);
    const open = computed(() => props.open ?? uncontrolledOpen.value);
    
    function setOpen(value: boolean | ((value: boolean) => boolean)) {
      const openState = typeof value === 'function' ? value(open.value) : value;
      emit('open-change', openState);
      if (props.open !== undefined) return;
    
      uncontrolledOpen.value = openState;
      // Persist non-sensitive uncontrolled open state for server-side restore.
      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}; SameSite=Lax`;
    }
    
    function setOpenNarrow(value: boolean) {
      openNarrow.value = value;
    }
    
    function toggleSidebar() {
      if (isNarrow.value) setOpenNarrow(!openNarrow.value);
      else setOpen((value) => !value);
    }
    
    useEventListener('keydown', (event: KeyboardEvent) => {
      if (event.key !== SIDEBAR_KEYBOARD_SHORTCUT) return;
      if (!event.metaKey && !event.ctrlKey) return;
      // Don't hijack Cmd/Ctrl+B (native bold) while typing in an editor.
      if (isEditableTarget(event.target)) return;
    
      event.preventDefault();
      toggleSidebar();
    });
    
    // Drives data-state="expanded" | "collapsed" for the parts' styling hooks.
    const state = computed(() => (open.value ? 'expanded' : 'collapsed'));
    
    provide(SidebarKey, {
      state,
      open,
      setOpen,
      isNarrow,
      openNarrow,
      setOpenNarrow,
      toggleSidebar,
    });
    </script>
    
    <template>
      <TooltipProvider :delay-duration="0">
        <div
          data-slot="sidebar-wrapper"
          :style="{
            '--sidebar-width': SIDEBAR_WIDTH,
            '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
          }"
          :class="
            cn(
              'nx:group/sidebar-wrapper nx:flex nx:min-h-svh nx:w-full nx:has-data-[variant=inset]:bg-nav-background',
              props.class
            )
          "
          v-bind="$attrs"
        >
          <slot />
        </div>
      </TooltipProvider>
    </template>
    
    components/sidebar/SidebarRail.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import { useSidebar } from '.';
    
    export interface SidebarRailProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<SidebarRailProps>();
    
    const { toggleSidebar } = useSidebar();
    </script>
    
    <template>
      <button
        data-slot="sidebar-rail"
        aria-label="Toggle Sidebar"
        tabindex="-1"
        title="Toggle Sidebar"
        :class="
          cn(
            'nx:absolute nx:inset-y-0 nx:z-sticky nx:hidden nx:w-4 nx:border-0 nx:bg-transparent nx:p-0 nx:-translate-x-1/2 nx:transition-[left,right,translate,background-color] nx:duration-fast nx:ease-linear nx:group-data-[side=left]:-right-4 nx:group-data-[side=right]:left-0 nx:after:absolute nx:after:inset-y-0 nx:after:left-1/2 nx:after:w-0.5 nx:hover:after:bg-nav-border nx:md:flex',
            'nx:in-data-[side=left]:cursor-w-resize nx:in-data-[side=right]:cursor-e-resize',
            'nx:[[data-side=left][data-state=collapsed]_&]:cursor-e-resize nx:[[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
            'nx:group-data-[collapsible=offcanvas]:translate-x-0 nx:group-data-[collapsible=offcanvas]:after:left-full nx:hover:group-data-[collapsible=offcanvas]:bg-nav-background',
            'nx:[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
            'nx:[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
            props.class
          )
        "
        @click="toggleSidebar"
      />
    </template>
    
    components/sidebar/SidebarSeparator.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    import { Separator } from '../separator';
    
    export interface SidebarSeparatorProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<SidebarSeparatorProps>();
    </script>
    
    <template>
      <Separator
        data-slot="sidebar-separator"
        :class="
          cn(
            'nx:mx-2 nx:data-[orientation=horizontal]:w-auto nx:bg-nav-border',
            props.class
          )
        "
      />
    </template>
    
    components/sidebar/SidebarTrigger.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import type { HTMLAttributes, VNode } from 'vue';
    
    import { IconLayoutSidebar } from '../../lib/icons';
    import { cn } from '../../lib/utils';
    import { Button, type ButtonProps } from '../button';
    
    import { useSidebar } from '.';
    
    export interface SidebarTriggerProps extends Omit<ButtonProps, 'class'> {
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<SidebarTriggerProps>(), {
      as: 'button',
      variant: 'ghost',
      size: 'icon',
      loading: false,
      type: 'button',
    });
    
    const slots = defineSlots<{
      'start-icon'?: () => VNode[];
      'end-icon'?: () => VNode[];
    }>();
    
    const delegated = reactiveOmit(props, 'class');
    
    const { toggleSidebar } = useSidebar();
    </script>
    
    <template>
      <Button
        v-bind="delegated"
        data-slot="sidebar-trigger"
        :class="cn('nx:size-7', props.class)"
        @click="toggleSidebar"
      >
        <template v-if="slots['start-icon']" #start-icon>
          <slot name="start-icon" />
        </template>
        <IconLayoutSidebar />
        <span class="nx:sr-only">Toggle Sidebar</span>
        <template v-if="slots['end-icon']" #end-icon>
          <slot name="end-icon" />
        </template>
      </Button>
    </template>
    
    components/sidebar/index.ts
    import { cva, type VariantProps } from 'class-variance-authority';
    import { inject } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import { SidebarKey } from './sidebar-context';
    
    export { default as Sidebar, type SidebarProps } from './Sidebar.vue';
    export {
      default as SidebarContent,
      type SidebarContentProps,
    } from './SidebarContent.vue';
    export {
      default as SidebarFooter,
      type SidebarFooterProps,
    } from './SidebarFooter.vue';
    export {
      default as SidebarGroup,
      type SidebarGroupProps,
    } from './SidebarGroup.vue';
    export {
      default as SidebarGroupAction,
      type SidebarGroupActionProps,
    } from './SidebarGroupAction.vue';
    export {
      default as SidebarGroupContent,
      type SidebarGroupContentProps,
    } from './SidebarGroupContent.vue';
    export {
      default as SidebarGroupLabel,
      type SidebarGroupLabelProps,
    } from './SidebarGroupLabel.vue';
    export {
      default as SidebarHeader,
      type SidebarHeaderProps,
    } from './SidebarHeader.vue';
    export {
      default as SidebarInput,
      type SidebarInputProps,
    } from './SidebarInput.vue';
    export {
      default as SidebarInset,
      type SidebarInsetProps,
    } from './SidebarInset.vue';
    export {
      default as SidebarMenu,
      type SidebarMenuProps,
    } from './SidebarMenu.vue';
    export {
      default as SidebarMenuAction,
      type SidebarMenuActionProps,
    } from './SidebarMenuAction.vue';
    export {
      default as SidebarMenuBadge,
      type SidebarMenuBadgeProps,
    } from './SidebarMenuBadge.vue';
    export {
      default as SidebarMenuButton,
      type SidebarMenuButtonProps,
    } from './SidebarMenuButton.vue';
    export {
      default as SidebarMenuItem,
      type SidebarMenuItemProps,
    } from './SidebarMenuItem.vue';
    export {
      default as SidebarMenuSkeleton,
      type SidebarMenuSkeletonProps,
    } from './SidebarMenuSkeleton.vue';
    export {
      default as SidebarMenuSub,
      type SidebarMenuSubProps,
    } from './SidebarMenuSub.vue';
    export {
      default as SidebarMenuSubButton,
      type SidebarMenuSubButtonProps,
    } from './SidebarMenuSubButton.vue';
    export {
      default as SidebarMenuSubItem,
      type SidebarMenuSubItemProps,
    } from './SidebarMenuSubItem.vue';
    export {
      default as SidebarProvider,
      type SidebarProviderProps,
    } from './SidebarProvider.vue';
    export {
      default as SidebarRail,
      type SidebarRailProps,
    } from './SidebarRail.vue';
    export {
      default as SidebarSeparator,
      type SidebarSeparatorProps,
    } from './SidebarSeparator.vue';
    export {
      default as SidebarTrigger,
      type SidebarTriggerProps,
    } from './SidebarTrigger.vue';
    
    /**
     * Reads the sidebar state and the `toggleSidebar` helper. Call it inside a
     * `SidebarProvider`, or it throws.
     */
    export function useSidebar() {
      const context = inject(SidebarKey, null);
      if (!context) {
        throw new Error('useSidebar must be used within a SidebarProvider.');
      }
    
      return context;
    }
    
    export const sidebarMenuButtonVariants = cva(
      cn(
        'nx:peer/menu-button nx:flex nx:w-full nx:items-center nx:gap-2 nx:overflow-hidden nx:rounded-md nx:border-0 nx:bg-transparent nx:p-2 nx:text-left nx:text-nav-muted-foreground nx:no-underline nx:transition-[width,height,padding] nx:duration-fast',
        'nx:group-has-data-[slot=sidebar-menu-action]/menu-item:pr-8 nx:group-data-[collapsible=icon]:size-8',
        'nx:group-data-[collapsible=icon]:justify-center nx:group-data-[collapsible=icon]:[&>span]:sr-only nx:group-data-[collapsible=icon]:[&>svg:not(:first-child)]:hidden',
        'nx:hover:bg-nav-item-hover nx:active:bg-nav-item-active',
        'nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default',
        'nx:disabled:pointer-events-none nx:disabled:text-disabled-foreground nx:aria-disabled:pointer-events-none nx:aria-disabled:text-disabled-foreground',
        'nx:data-[active=true]:bg-nav-item-active nx:data-[active=true]:text-nav-foreground',
        'nx:data-[state=open]:hover:bg-nav-item-hover',
        'nx:[&>span:last-child]:truncate nx:[&>svg]:size-4 nx:[&>svg]:shrink-0'
      ),
      {
        variants: {
          variant: {
            default: 'nx:hover:bg-nav-item-hover',
            outline:
              'nx:border-default nx:border-nav-border nx:bg-nav-background nx:hover:bg-nav-item-hover',
          },
          size: {
            default:
              'nx:h-8 nx:typography-body-default nx:group-data-[collapsible=icon]:p-2',
            sm: 'nx:h-7 nx:typography-body-small nx:group-data-[collapsible=icon]:p-2',
            lg: 'nx:h-12 nx:typography-body-default nx:group-data-[collapsible=icon]:p-0',
          },
        },
        defaultVariants: {
          variant: 'default',
          size: 'default',
        },
      }
    );
    
    export type SidebarMenuButtonVariants = VariantProps<
      typeof sidebarMenuButtonVariants
    >;
    
    components/sidebar/sidebar-context.ts
    import type { InjectionKey, Ref } from 'vue';
    
    /** What `useSidebar` returns: the open state and the helpers that change it. */
    export interface SidebarContextValue {
      state: Readonly<Ref<'expanded' | 'collapsed'>>;
      open: Readonly<Ref<boolean>>;
      setOpen: (value: boolean | ((value: boolean) => boolean)) => void;
      openNarrow: Readonly<Ref<boolean>>;
      setOpenNarrow: (open: boolean) => void;
      isNarrow: Readonly<Ref<boolean>>;
      toggleSidebar: () => void;
    }
    
    export const SidebarKey: InjectionKey<SidebarContextValue> = Symbol('sidebar');
    
  3. Keep each file at the path shown above it.

    Paths are relative to packages/vue/src/. Keep the same folders under your @/ root, and the imports between files work without edits.

Usage

import { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger } from '@/components/sidebar';

Examples

With submenu

Nest pages under a menu item with SidebarMenuSub.

<script setup lang="ts">
import { PhCaretRight, PhFolder, PhTray } from '@phosphor-icons/vue';

import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarSeparator,
  SidebarTrigger,
} from '@/components/sidebar';
</script>

<template>
  <div
    class="nx:h-96 nx:w-full nx:overflow-hidden nx:rounded-lg nx:border-default nx:border-border-default"
  >
    <SidebarProvider :style="{ minHeight: 'unset', height: '100%' }">
      <Sidebar collapsible="none">
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Projects</SidebarGroupLabel>
            <SidebarGroupContent>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuButton is-active>
                    <PhFolder />
                    <span>Design System</span>
                    <PhCaretRight class="nx:ml-auto" />
                  </SidebarMenuButton>
                  <SidebarMenuSub>
                    <SidebarMenuSubItem>
                      <SidebarMenuSubButton href="#tokens" is-active
                        >Tokens</SidebarMenuSubButton
                      >
                    </SidebarMenuSubItem>
                    <SidebarMenuSubItem>
                      <SidebarMenuSubButton href="#components"
                        >Components</SidebarMenuSubButton
                      >
                    </SidebarMenuSubItem>
                    <SidebarMenuSubItem>
                      <SidebarMenuSubButton href="#documentation"
                        >Documentation</SidebarMenuSubButton
                      >
                    </SidebarMenuSubItem>
                  </SidebarMenuSub>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
          <SidebarSeparator />
          <SidebarGroup>
            <SidebarGroupContent>
              <SidebarMenu>
                <SidebarMenuItem>
                  <SidebarMenuButton>
                    <PhTray />
                    <span>Inbox</span>
                  </SidebarMenuButton>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
        <SidebarRail />
      </Sidebar>
      <SidebarInset>
        <header
          class="nx:flex nx:h-16 nx:items-center nx:gap-2 nx:border-b-default nx:border-border-default nx:px-4"
        >
          <SidebarTrigger />
          <span class="nx:typography-label-default">Dashboard</span>
        </header>
        <div
          class="nx:min-h-[20rem] nx:p-4 nx:typography-label-default nx:text-muted-foreground"
        >
          Main content area.
        </div>
      </SidebarInset>
    </SidebarProvider>
  </div>
</template>

Compact

Set size="sm" on every row for a denser sidebar with 12px text.

<script setup lang="ts">
import {
  PhCalendarBlank,
  PhCaretRight,
  PhFolder,
  PhGear,
  PhHouse,
  PhMagnifyingGlass,
  PhTray,
  PhUser,
} from '@phosphor-icons/vue';

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInput,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarMenuSub,
  SidebarMenuSubButton,
  SidebarMenuSubItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
} from '@/components/sidebar';

const items = [
  { title: 'Home', icon: PhHouse },
  { title: 'Inbox', icon: PhTray, badge: '4' },
  { title: 'Calendar', icon: PhCalendarBlank },
  { title: 'Search', icon: PhMagnifyingGlass },
  { title: 'Settings', icon: PhGear },
];
</script>

<template>
  <div
    class="nx:h-96 nx:w-full nx:overflow-hidden nx:rounded-lg nx:border-default nx:border-border-default"
  >
    <SidebarProvider :style="{ minHeight: 'unset', height: '100%' }">
      <Sidebar collapsible="none">
        <SidebarHeader>
          <SidebarInput aria-label="Search" placeholder="Search…" />
        </SidebarHeader>
        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupLabel>Platform</SidebarGroupLabel>
            <SidebarGroupContent>
              <SidebarMenu>
                <SidebarMenuItem v-for="item in items" :key="item.title">
                  <SidebarMenuButton
                    size="sm"
                    :tooltip="item.title"
                    :is-active="item.title === 'Home'"
                  >
                    <component :is="item.icon" />
                    <span>{{ item.title }}</span>
                  </SidebarMenuButton>
                  <SidebarMenuBadge v-if="item.badge">{{
                    item.badge
                  }}</SidebarMenuBadge>
                </SidebarMenuItem>
                <SidebarMenuItem>
                  <SidebarMenuButton size="sm" tooltip="Projects">
                    <PhFolder />
                    <span>Projects</span>
                    <PhCaretRight class="nx:ml-auto" />
                  </SidebarMenuButton>
                  <SidebarMenuSub>
                    <SidebarMenuSubItem>
                      <SidebarMenuSubButton href="#overview" size="sm"
                        >Overview</SidebarMenuSubButton
                      >
                    </SidebarMenuSubItem>
                    <SidebarMenuSubItem>
                      <SidebarMenuSubButton href="#mobile" size="sm" is-active
                        >Mobile App</SidebarMenuSubButton
                      >
                    </SidebarMenuSubItem>
                  </SidebarMenuSub>
                </SidebarMenuItem>
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>
        <SidebarFooter>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton size="sm" tooltip="Account">
                <PhUser />
                <span>Account</span>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarFooter>
        <SidebarRail />
      </Sidebar>
      <SidebarInset>
        <header
          class="nx:flex nx:h-16 nx:items-center nx:gap-2 nx:border-b-default nx:border-border-default nx:px-4"
        >
          <SidebarTrigger />
          <span class="nx:typography-label-default">Dashboard</span>
        </header>
        <div
          class="nx:min-h-[20rem] nx:p-4 nx:typography-label-default nx:text-muted-foreground"
        >
          Compact rows use the small size (12px body-small) for denser,
          information-heavy sidebars.
        </div>
      </SidebarInset>
    </SidebarProvider>
  </div>
</template>

Flat

Skip the group parts and put nx:p-2 on SidebarContent for a flat list of links.

<script setup lang="ts">
import {
  PhCalendarBlank,
  PhGear,
  PhHouse,
  PhMagnifyingGlass,
  PhTray,
} from '@phosphor-icons/vue';

import {
  Sidebar,
  SidebarContent,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
} from '@/components/sidebar';

const items = [
  { title: 'Home', icon: PhHouse },
  { title: 'Inbox', icon: PhTray, badge: '4' },
  { title: 'Calendar', icon: PhCalendarBlank },
  { title: 'Search', icon: PhMagnifyingGlass },
  { title: 'Settings', icon: PhGear },
];
</script>

<template>
  <div
    class="nx:h-96 nx:w-full nx:overflow-hidden nx:rounded-lg nx:border-default nx:border-border-default"
  >
    <SidebarProvider :style="{ minHeight: 'unset', height: '100%' }">
      <Sidebar collapsible="none">
        <SidebarContent class="nx:p-2">
          <SidebarMenu>
            <SidebarMenuItem v-for="item in items" :key="item.title">
              <SidebarMenuButton
                :tooltip="item.title"
                :is-active="item.title === 'Home'"
              >
                <component :is="item.icon" />
                <span>{{ item.title }}</span>
              </SidebarMenuButton>
              <SidebarMenuBadge v-if="item.badge">{{
                item.badge
              }}</SidebarMenuBadge>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarContent>
        <SidebarRail />
      </Sidebar>
      <SidebarInset>
        <header
          class="nx:flex nx:h-16 nx:items-center nx:gap-2 nx:border-b-default nx:border-border-default nx:px-4"
        >
          <SidebarTrigger />
          <span class="nx:typography-label-default">Dashboard</span>
        </header>
        <div
          class="nx:min-h-[20rem] nx:p-4 nx:typography-label-default nx:text-muted-foreground"
        >
          Flat structure — no SidebarGroup/-Label/-Content, just a SidebarMenu
          of buttons.
        </div>
      </SidebarInset>
    </SidebarProvider>
  </div>
</template>

Props

Sidebar

Sidebar props
PropTypeDefaultDescription
collapsible"icon" | "offcanvas" | "none""offcanvas"

How it collapses: slides off screen, shrinks to an icon rail, or never.

side"right" | "left""left"

Edge the sidebar docks to.

variant"floating" | "inset" | "sidebar""sidebar"

Visual style. inset pairs with SidebarInset to show the main region as a card.

SidebarContent

SidebarContent adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarFooter adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarGroup

SidebarGroup adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarGroupAction

SidebarGroupAction adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarGroupContent

SidebarGroupContent adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarGroupLabel

SidebarGroupLabel adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarHeader

SidebarHeader adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarInput

SidebarInput props
PropTypeDefaultDescription
size"default" | "sm" | "lg" | null"default"No description
variant"ghost" | "bordered" | "borderless" | null"bordered"No description

SidebarInset

SidebarInset adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarMenu

SidebarMenu adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarMenuAction

SidebarMenuAction props
PropTypeDefaultDescription
showOnHoverbooleanfalse

Shows the action only on hover or focus of the menu item.

SidebarMenuBadge

SidebarMenuBadge adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarMenuButton

SidebarMenuButton props
PropTypeDefaultDescription
isActivebooleanfalse

Marks the item as the current page.

size"default" | "sm" | "lg" | null"default"No description
tooltipstring | TooltipContentPropsNo default documented

Tooltip shown only on the icon rail. A string or TooltipContent props.

variant"default" | "outline" | null"default"No description

SidebarMenuItem

SidebarMenuItem adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarMenuSkeleton

SidebarMenuSkeleton props
PropTypeDefaultDescription
showIconbooleanfalse

Adds an icon-sized block before the text bar.

SidebarMenuSub

SidebarMenuSub adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarMenuSubButton

SidebarMenuSubButton props
PropTypeDefaultDescription
isActivebooleanfalse

Marks the sub-item as the current page.

size"sm" | "md""md"

Text size of the sub-item.

SidebarMenuSubItem

SidebarMenuSubItem adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarProvider

SidebarProvider props
PropTypeDefaultDescription
defaultOpenbooleantrue

Opens the sidebar on first render when uncontrolled. The open state is saved to a sidebar_state cookie. Read that cookie on the server and pass it here to restore it.

openbooleanNo default documented

Controlled open state.

SidebarRail

SidebarRail adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarSeparator

SidebarSeparator adds no props of its own; it accepts the props of the element or primitive it wraps.

SidebarTrigger

SidebarTrigger props
PropTypeDefaultDescription
disabledbooleanNo default documentedNo description
loadingbooleanfalse

Disables the button and shows a spinner in place of its content.

size"default" | "sm" | "lg" | "icon-sm" | "icon" | "icon-lg" | null"icon"No description
type"button" | "submit" | "reset""button"No description
variant"default" | "destructive" | "error" | "outline" | "dashed" | "secondary" | "ghost" | "link" | null"ghost"No description