Sidebar

A composable, themeable sidebar for app navigation.

This component uses shadcn/uiDocs
Loading...
'use client'
 
import { Calendar, Home, Inbox, Search, Settings } from 'lucide-react'
import {
  Sidebar,
  SidebarContent,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarInset,
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
} from 'ui'
 
const items = [
  { title: 'Home', url: '#', icon: Home },
  { title: 'Inbox', url: '#', icon: Inbox },
  { title: 'Calendar', url: '#', icon: Calendar },
  { title: 'Search', url: '#', icon: Search },
  { title: 'Settings', url: '#', icon: Settings },
]
 
const menuButtonClassName =
  '[&>svg]:size-4 text-foreground-muted data-[active=true]:text-foreground'
 
export function SidebarDemo() {
  return (
    <div className="h-[480px] w-full">
      <SidebarProvider defaultOpen className="h-full min-h-0!">
        <Sidebar collapsible="icon">
          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupLabel>Application</SidebarGroupLabel>
              <SidebarGroupContent>
                <SidebarMenu>
                  {items.map((item) => (
                    <SidebarMenuItem key={item.title}>
                      <SidebarMenuButton
                        asChild
                        isActive={item.title === 'Home'}
                        tooltip={item.title}
                        className={menuButtonClassName}
                      >
                        <a href={item.url}>
                          <item.icon strokeWidth={1.5} />
                          <span>{item.title}</span>
                        </a>
                      </SidebarMenuButton>
                    </SidebarMenuItem>
                  ))}
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
          <SidebarRail />
        </Sidebar>
        <SidebarInset className="min-h-0">
          <header className="flex h-12 shrink-0 items-center gap-2 border-b px-4">
            <SidebarTrigger className="-ml-1 text-foreground-muted" />
          </header>
        </SidebarInset>
      </SidebarProvider>
    </div>
  )
}

Composable sidebar primitives for app chrome. The implementation lives in packages/ui and is used by Studio. Import from 'ui'.

Usage

import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarHeader,
  SidebarProvider,
  SidebarTrigger,
} from 'ui'
export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <SidebarProvider>
      <AppSidebar />
      <main>
        <SidebarTrigger />
        {children}
      </main>
    </SidebarProvider>
  )
}
export function AppSidebar() {
  return (
    <Sidebar>
      <SidebarHeader />
      <SidebarContent>
        <SidebarGroup />
      </SidebarContent>
      <SidebarFooter />
    </Sidebar>
  )
}

Structure

PartRole
SidebarProviderOpen / collapsed state and keyboard shortcut
SidebarCollapsible shell (side, variant, collapsible)
SidebarHeader / SidebarFooterSticky top and bottom regions
SidebarContentScrollable middle
SidebarGroupSection inside content
SidebarMenu and friendsNav items, actions, badges, submenus
SidebarTrigger / SidebarRailToggle controls

Theming

Sidebar colours use full colour CSS variables (aliases into the semantic system), not classic shadcn HSL channel lists. Do not wrap them in hsl().

Shared defaults (also in packages/ui compat CSS, matching Studio):

:root {
  --sidebar-background: var(--background-dash-sidebar);
  --sidebar-foreground: var(--foreground-default);
  --sidebar-primary: var(--foreground-default);
  --sidebar-primary-foreground: var(--warning);
  --sidebar-accent: var(--background-selection);
  --sidebar-accent-foreground: var(--foreground-default);
  --sidebar-border: var(--border-default);
  --sidebar-ring: var(--ring);
}

Apps that import config/tailwind.config.css already map these to --color-sidebar* in packages/config/css/theme.css, so utilities like bg-sidebar and text-sidebar-foreground work without a local Tailwind colour entry.

Override any --sidebar-* token when the sidebar should diverge from the main app surface.

SidebarProvider

Always wrap the sidebar and its trigger in SidebarProvider.

PropTypeDescription
defaultOpenbooleanUncontrolled initial open state
openbooleanControlled open state
onOpenChange(open: boolean) => voidControlled open setter

Width

Override desktop width with --sidebar-width on the provider (also used for layout spacing):

<SidebarProvider
  style={
    {
      '--sidebar-width': '20rem',
    } as React.CSSProperties
  }
>
  <Sidebar />
</SidebarProvider>

Defaults live as SIDEBAR_WIDTH / SIDEBAR_WIDTH_ICON in packages/ui/src/components/shadcn/ui/sidebar.tsx. On mobile, the sheet sets --sidebar-width from the hard-coded SIDEBAR_WIDTH_MOBILE constant, so there is no separate CSS variable to override mobile width today.

Persisted state

SidebarProvider writes open state to a cookie (SIDEBAR_COOKIE_NAME, default sidebar:state in this repo). For Next.js App Router you can seed defaultOpen from that cookie on the server.

PropTypeDescription
side'left' | 'right'Which edge
variant'sidebar' | 'floating' | 'inset'Visual treatment
collapsible'offcanvas' | 'icon' | 'none'Collapse behaviour
collapsibleBehaviour
offcanvasSlides off-canvas
iconCollapses to icons
noneAlways expanded

For variant="inset", wrap main content in SidebarInset.

<SidebarProvider>
  <Sidebar variant="inset" />
  <SidebarInset>
    <main>{children}</main>
  </SidebarInset>
</SidebarProvider>

useSidebar

import { useSidebar } from 'ui'
 
export function CustomTrigger() {
  const { toggleSidebar } = useSidebar()
  return <button onClick={toggleSidebar}>Toggle Sidebar</button>
}

Must be used under SidebarProvider.

Typical nav group:

<SidebarGroup>
  <SidebarGroupLabel>Application</SidebarGroupLabel>
  <SidebarGroupContent>
    <SidebarMenu>
      {items.map((item) => (
        <SidebarMenuItem key={item.title}>
          <SidebarMenuButton asChild>
            <a href={item.url}>
              <item.icon />
              <span>{item.title}</span>
            </a>
          </SidebarMenuButton>
        </SidebarMenuItem>
      ))}
    </SidebarMenu>
  </SidebarGroupContent>
</SidebarGroup>

Also available: SidebarMenuAction, SidebarMenuBadge, SidebarMenuSkeleton, SidebarMenuSub / SidebarMenuSubButton, SidebarGroupAction, SidebarSeparator, SidebarRail.

Controlled sidebar

export function AppSidebar() {
  const [open, setOpen] = React.useState(true)
 
  return (
    <SidebarProvider open={open} onOpenChange={setOpen}>
      <Sidebar />
    </SidebarProvider>
  )
}

Styling with state

Hide a group when the sidebar is icon-collapsed:

<Sidebar collapsible="icon">
  <SidebarContent>
    <SidebarGroup className="group-data-[collapsible=icon]:hidden" />
  </SidebarContent>
</Sidebar>

Keep a menu action visible when its button is active:

<SidebarMenuItem>
  <SidebarMenuButton />
  <SidebarMenuAction className="peer-data-[active=true]/menu-button:opacity-100" />
</SidebarMenuItem>

Source

packages/ui/src/components/shadcn/ui/sidebar.tsx