Components
A composable, themeable app sidebar that collapses to icons, slides off-canvas, or floats — with header, footer, groups, menus, and nested submenus. On mobile it renders inside a Sheet.
Playground
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from "@workspace/ui/components/sidebar"
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-12 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ms-1" />
</header>
{children}
</SidebarInset>
</SidebarProvider>
)
}
function AppSidebar() {
return (
<Sidebar>
<SidebarHeader>{/* brand */}</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild isActive={item.isActive}>
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>{/* user */}</SidebarFooter>
<SidebarRail />
</Sidebar>
)
}<Sidebar>
<SidebarHeader>
<SidebarInput placeholder="Search..." />
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarGroupAction title="Add project">
<PlusIcon />
<span className="sr-only">Add project</span>
</SidebarGroupAction>
<SidebarGroupContent>
<SidebarMenu>{/* items */}</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<SidebarSeparator />
<SidebarGroup>
<SidebarGroupLabel>Support</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>{/* items */}</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>group/collapsible data state. SidebarMenuSubButton renders an anchor by default and takes isActive and size just like its parent.<SidebarMenu>
{items.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<item.icon />
<span>{item.title}</span>
<CaretRightIcon className="ms-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items.map((subItem) => (
<SidebarMenuSubItem key={subItem}>
<SidebarMenuSubButton href="#">
<span>{subItem}</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>showOnHover keeps it hidden until the item is hovered or focused. Both stay clear of the menu button, which reserves end padding whenever an action is present.<SidebarMenuItem>
<SidebarMenuButton isActive>
<TrayIcon />
<span>Inbox</span>
</SidebarMenuButton>
<SidebarMenuBadge>24</SidebarMenuBadge>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton>
<FolderIcon />
<span>Website Redesign</span>
</SidebarMenuButton>
<SidebarMenuAction showOnHover title="More">
<DotsThreeIcon />
<span className="sr-only">More</span>
</SidebarMenuAction>
</SidebarMenuItem>collapsible="icon" shrinks the sidebar to a rail of icons instead of hiding it — pass tooltip to each SidebarMenuButton so labels surface on hover while collapsed. This preview starts collapsed (defaultOpen={false} on the provider); expand it with the trigger or the rail.<SidebarProvider defaultOpen={false}>
<Sidebar collapsible="icon">
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton tooltip={item.title}>
<item.icon />
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarRail />
</Sidebar>
{/* ... */}
</SidebarProvider>showIcon adds a leading icon placeholder. This preview also shows collapsible="none": a static column with no collapse behavior.<Sidebar collapsible="none">
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{Array.from({ length: 5 }).map((_, index) => (
<SidebarMenuItem key={index}>
<SidebarMenuSkeleton showIcon />
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
</Sidebar>dir="rtl" ancestor — pair it with side="right" so the sidebar sits on the reading start edge. The trigger icon flips automatically.<div dir="rtl">
<SidebarProvider>
<Sidebar side="right">{/* ... */}</Sidebar>
<SidebarInset>
<header className="flex h-12 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ms-1" />
<span className="text-sm font-medium">لوحة التحكم</span>
</header>
</SidebarInset>
</SidebarProvider>
</div>⌘B / Ctrl+B toggles the sidebar (every provider on this page responds, so the shortcut flips all previews at once). SidebarTrigger carries an sr-only “Toggle Sidebar” label and SidebarRail an aria-label; the rail is removed from the tab order since the trigger covers keyboard use. Menus are semantic ul/li lists, focus rings use --sidebar-ring, and on mobile the sidebar renders in a Sheet with an sr-only title and description. Tooltips only appear while collapsed to icons.// Menu buttons render real <button> elements — or your own element via asChild
<SidebarMenuButton asChild isActive={pathname === item.url}>
<Link href={item.url}>
<item.icon />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>useSidebar hook exposes the context for custom controls.| Component | Prop | Default |
|---|---|---|
| SidebarProvider | defaultOpen?: boolean, open?: boolean, onOpenChange?: (open) => void | true |
| Sidebar | side?: "left" | "right", variant?: "sidebar" | "floating" | "inset", collapsible?: "offcanvas" | "icon" | "none" | left / sidebar / offcanvas |
| SidebarMenuButton | asChild?, isActive?, variant?: "default" | "outline", size?: "default" | "sm" | "lg", tooltip?: string | TooltipContent props | default / default |
| SidebarMenuAction | asChild?, showOnHover?: boolean | false |
| SidebarMenuSubButton | asChild?, size?: "sm" | "md", isActive? | md |
| SidebarMenuSkeleton | showIcon?: boolean | false |
| SidebarGroupLabel / SidebarGroupAction | asChild?: boolean | false |
// Read or drive the sidebar from any descendant
const {
state, // "expanded" | "collapsed"
open, // desktop open state
setOpen,
openMobile, // mobile sheet state
setOpenMobile,
isMobile,
toggleSidebar, // toggles desktop or mobile, whichever is active
} = useSidebar()
// Widths are CSS variables — override them on the provider
<SidebarProvider
style={{
"--sidebar-width": "20rem",
"--sidebar-width-icon": "3.5rem",
} as React.CSSProperties}
>Tokens used
14 design tokens consumed by the Sidebar component.
--sidebar
Sidebar
--sidebar-foreground
Sidebar Foreground
--sidebar-primary
Sidebar Primary
--sidebar-primary-foreground
Sidebar Primary Foreground
--sidebar-accent
Sidebar Accent
--sidebar-accent-foreground
Sidebar Accent Foreground
--sidebar-border
Sidebar Border
--sidebar-ring
Sidebar Ring
--background
Background
--text-sm
Text Small (14px)
--text-xs
Text Extra Small (12px)
--radius-md
Radius 0.8× (8px)
--radius-lg
Radius 1× (10px)
--spacing
Spacing unit (4px)