A composable application sidebar. Collapses off-canvas or to an icon rail, comes in sidebar, floating and inset styles, turns into a modal sheet on small screens, and toggles with ⌘B / Ctrl+B. Menu items can be links or buttons, with badges, groups and tooltips in icon mode.
SidebarProvider owns the open state and renders the layout wrapper (min-h-svh, full width, flex row). Sidebar reserves its width in that row and positions the panel fixed to the viewport, so the page content in SidebarInset never sits underneath it.
Rendering inside a box
The sidebar is fixed to the viewport by default. To keep it inside a
bounded element (a preview, a modal, a dashboard widget), give an ancestor
data-sidebar-contained and relative. The panel then uses absolute
positioning and the ancestor's height. Every preview on this page does
this, and sets className="h-full min-h-0" on SidebarProvider.
variant changes how the panel sits in the layout. sidebar (default) is a full-height column with a border. floating insets the panel with a margin, rounded corners and a shadow.
inset puts the panel on the sidebar background and turns SidebarInset into a raised, rounded card. Use it for dashboards where the content should read as one surface.
It works on either side. With side="right", render SidebarInset before the Sidebar (as for any right sidebar); the card drops its margin on the side next to the panel.
collapsible controls what happens when the sidebar is closed on desktop: offcanvas (default) slides it out of view, icon shrinks it to a 3rem rail of icons, and none renders a static panel that can't collapse. Switch modes in the preview and press the trigger.
In icon mode, group labels and badges hide, menu buttons become 32px squares, and text in the last <span> of each button is clipped. Set tooltip on every SidebarMenuButton so collapsed items keep a visible label on hover and focus.
side="right" docks the panel on the right, for details and inspector panels. Render SidebarInset before the Sidebar so the space is reserved on the correct side. On mobile the sheet slides in from the right. Pass an icon as the trigger's children to point it the right way.
ENG-482 · Checkout retries
Payments occasionally fail with a timeout from the card network. Retry idempotent requests up to three times with exponential backoff.
Details
Properties
Assignee
MC Maya Chen
Priority
Urgent
Due
Oct 14, 2026
Labels
checkout
Activity
Maya moved this to In progress · 2h ago
import { CalendarIcon, FlagIcon, PanelRightIcon, TagIcon, UserIcon,} from "lucide-react";import { Avatar } from "@/components/ui/avatar";import { Badge } from "@/components/ui/badge";import { Sidebar, SidebarContent, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarProvider, SidebarSeparator, SidebarTrigger,} from "@/components/ui/sidebar";const fields = [ { label: "Assignee", icon: UserIcon, value: ( <span className="flex items-center gap-1.5"> <Avatar size="xs" colorful fallback="MC" alt="Maya Chen" /> Maya Chen </span> ), }, { label: "Priority", icon: FlagIcon, value: ( <Badge variant="dot" color="danger"> Urgent </Badge> ), }, { label: "Due", icon: CalendarIcon, value: "Oct 14, 2026" }, { label: "Labels", icon: TagIcon, value: <Badge size="sm">checkout</Badge> },];export default function SidebarRight() { return ( <div data-sidebar-contained className="relative h-[400px] w-full overflow-hidden rounded-lg border" > <SidebarProvider className="h-full min-h-0"> {/* On the right, render SidebarInset first so the space is reserved on the right. */} <SidebarInset> <header className="flex h-12 items-center justify-between gap-2 border-b px-4"> <span className="font-medium text-sm"> ENG-482 · Checkout retries </span> <SidebarTrigger aria-label="Toggle details"> <PanelRightIcon /> </SidebarTrigger> </header> <div className="grid content-start gap-3 p-4 text-muted-foreground text-sm"> <p> Payments occasionally fail with a timeout from the card network. Retry idempotent requests up to three times with exponential backoff. </p> <div className="h-24 rounded-lg bg-muted" /> </div> </SidebarInset> <Sidebar side="right"> <SidebarHeader className="h-12 justify-center border-b px-4"> <span className="font-medium text-sm">Details</span> </SidebarHeader> <SidebarContent> <SidebarGroup> <SidebarGroupLabel>Properties</SidebarGroupLabel> <dl className="grid gap-3 px-2 text-sm"> {fields.map((f) => ( <div key={f.label} className="flex items-center justify-between gap-2" > <dt className="flex items-center gap-2 text-sidebar-foreground/70"> <f.icon className="size-4" /> {f.label} </dt> <dd>{f.value}</dd> </div> ))} </dl> </SidebarGroup> <SidebarSeparator /> <SidebarGroup> <SidebarGroupLabel>Activity</SidebarGroupLabel> <p className="px-2 text-sidebar-foreground/70 text-xs"> Maya moved this to In progress · 2h ago </p> </SidebarGroup> </SidebarContent> </Sidebar> </SidebarProvider> </div> );}
Split long navigation into SidebarGroups with a SidebarGroupLabel each. SidebarFooter stays pinned to the bottom while SidebarContent scrolls; a SidebarSeparator divides them.
size="lg" (48px) for workspace and user switchers with two lines of text, default (32px) for primary navigation, and sm (28px) for dense or secondary lists.
Nest a SidebarMenu inside a React Aria Disclosure, and give the SidebarMenuButtonslot="trigger" to toggle it. The button gets aria-expanded automatically. Animate the panel height with --disclosure-panel-height, as the Disclosure component does.
Mark the current page with isActive; for links it also sets aria-current="page". Derive it from your router's current path, and wrap the app in React Aria's RouterProvider so menu links use client-side navigation. The preview uses a local stand-in for the router.
Pass open and onOpenChange to SidebarProvider to own the desktop state, for example to persist it in a cookie or user setting. On desktop, the trigger, the keyboard shortcut and anything calling setOpen all go through onOpenChange. The mobile sheet keeps its own state.
Any component inside SidebarProvider can read and change the state with useSidebar(). Use it for custom triggers, or to adapt content to the collapsed state. It throws if called outside a provider.
collapsible="none" renders a plain, always-visible <div> with no container, spacer or mobile sheet. Use it for secondary navigation inside a page, such as settings.
It stays inline on every screen size, as in shadcn/ui: below 768px it doesn't become a sheet or hide, and SidebarTrigger and the shortcut don't affect it. It's a normal flex item, so it never overlays the page, but in a row it shrinks with the layout; on narrow screens you may want to stack it above the content or swap it for a Select or Tabs.
Below 768px (max-width: 767px) the desktop panel is hidden, and Sidebar renders as a modal sheet from its side instead. The trigger and the shortcut toggle the sheet (openMobile), which is separate from the desktop open state, so collapsing on desktop doesn't affect mobile. The sheet has a backdrop, traps focus, and closes on Esc or an outside click.
Pressing a link inside the sheet closes it, with the mouse, touch or Enter. This covers any <a href> in the sheet, whether it's a SidebarMenuButton link or a link in your own markup. Clicks that open a new tab or window (with a modifier key, target="_blank" or download) leave it open, and so do button items. Pass closeOnNavigate={false} to keep the sheet open, or call setOpenMobile(false) from useSidebar() to close it after a button press.
className and style on Sidebar also apply to the sheet. Use max-md: classes to style only the sheet, and md: classes to style only the desktop panel.
On a phone, open the sheet and pick a page: it closes as the route changes.
import { HomeIcon, InboxIcon, SettingsIcon, UsersIcon } from "lucide-react";import { useState } from "react";import { RouterProvider } from "react-aria-components";import { Sidebar, SidebarContent, SidebarGroup, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarTrigger,} from "@/components/ui/sidebar";const items = [ { href: "/home", title: "Home", icon: HomeIcon }, { href: "/inbox", title: "Inbox", icon: InboxIcon }, { href: "/team", title: "Team", icon: UsersIcon }, { href: "/settings", title: "Settings", icon: SettingsIcon },];export default function SidebarMobile() { // Stand-in for your router. In Next.js use `useRouter().push` and `usePathname()`. const [pathname, setPathname] = useState("/home"); const page = items.find((item) => item.href === pathname); return ( <RouterProvider navigate={setPathname}> <div data-sidebar-contained className="relative h-[360px] w-full overflow-hidden rounded-lg border" > <SidebarProvider className="h-full min-h-0"> {/* max-md: classes only reach the mobile sheet. */} <Sidebar className="max-md:w-72"> <SidebarContent> <SidebarGroup> <SidebarMenu> {items.map((item) => ( <SidebarMenuItem key={item.href}> <SidebarMenuButton href={item.href} isActive={item.href === pathname} > <item.icon /> <span>{item.title}</span> </SidebarMenuButton> </SidebarMenuItem> ))} </SidebarMenu> </SidebarGroup> </SidebarContent> </Sidebar> <SidebarInset> <header className="flex h-12 items-center gap-2 border-b px-3"> <SidebarTrigger /> <span className="font-medium text-sm">{page?.title}</span> </header> <p className="p-4 text-muted-foreground text-sm"> On a phone, open the sheet and pick a page: it closes as the route changes. </p> </SidebarInset> </SidebarProvider> </div> </RouterProvider> );}
The breakpoint is based on the viewport width (via useIsMobile()), not the container, so contained previews on a wide screen always show the desktop sidebar. Resize the browser window to see the sheet.
An inset sidebar with icon collapsing: a workspace switcher Menu in the header, primary navigation with a badge, a projects group, and an account menu in the footer. The content area has a trigger and Breadcrumbs.
Folders and labels with unread counts in SidebarMenuBadge. The Compose button reads useSidebar() to collapse to an icon and show its tooltip only in icon mode, matching the menu buttons.
SidebarMenuButton renders a real link when given href, with aria-current="page" when isActive, so middle-click, "open in new tab" and screen-reader link lists work. Without href it's a button.
Wrap navigation in landmarks as your layout needs; SidebarInset renders the page's <main>.
In icon mode, give every SidebarMenuButton a tooltip. The text stays in the DOM and keeps naming the item for assistive tech; the tooltip gives sighted users the same label.
Tooltips only show when the sidebar is collapsed to icons on desktop; they're disabled when expanded and on mobile.
SidebarTrigger has aria-label="Toggle sidebar". Pass your own aria-label to change it.
On mobile the sidebar is a modal Dialog labelled "Sidebar": focus moves into it, is trapped while open and returns to the trigger on close.
The ⌘B / Ctrl+B shortcut is registered on window by each SidebarProvider and calls preventDefault(), so it replaces any browser or editor shortcut on the same keys while the page is focused. Advertise it with a Kbd hint.
On the desktop Sidebar root (data-slot="sidebar", which also has the group and peer classes):
Attribute
Values
data-state
expanded or collapsed
data-collapsible
The collapsible mode while collapsed (offcanvas or icon); empty while expanded
data-variant
sidebar, floating or inset
data-side
left or right
Style anything inside the sidebar by state with group-data-*, for example group-data-[collapsible=icon]:hidden to hide an element in icon mode. Style siblings after it with peer-data-*. SidebarInset styles itself for the inset variant from the wrapper (group-has-* on sidebar-wrapper), so it works whether it comes before or after the Sidebar. The mobile sheet instead has data-slot="sidebar", data-mobile="true" and data-side.
<SidebarProvider style={{ "--sidebar-width": "18rem", "--sidebar-width-icon": "3.5rem" } as React.CSSProperties}/>
The colors come from theme tokens: --sidebar (background), --sidebar-foreground, --sidebar-border, --sidebar-accent and --sidebar-accent-foreground (active item), --sidebar-primary (active item icon) and --sidebar-ring. See Theming.
On desktop, className and other props on Sidebar go to sidebar-container, the fixed panel. With collapsible="none" they go to the root <div>. On mobile, className and style go to the sheet (the Modal); other <div> props aren't applied there.
SidebarMenuButton truncates its last <span>, so wrap labels in a <span> and put icons first.
SidebarMenuBadge is absolutely positioned for the default button height. Add padding to the button (pr-8) if long labels could run under it.
A ghost, icon-smButton with a panel icon. Pass children to replace the icon, for example <PanelRightIcon /> for a right sidebar, or text with size="sm". It keeps aria-label="Toggle sidebar" unless you pass your own, so with visible text pass a matching aria-label (or aria-label={undefined}). Accepts every button prop; onPress runs before the sidebar toggles.