Sidebar
The application navigation rail. Stack the parts you need: a workspace switcher up top, the primary pages first, labeled sections and favorites below, and a profile switcher pinned to the bottom.
Installation#
Usage#
import {
Sidebar, SidebarHeader, SidebarContent, SidebarFooter,
SidebarGroup, SidebarGroupLabel, SidebarItem, SidebarSwitcher,
} from "@/components/ui/sidebar"
export function AppSidebar() {
return (
<Sidebar aria-label="Main">
<SidebarHeader>{/* workspace switcher */}</SidebarHeader>
<SidebarContent>
<SidebarGroup aria-label="Primary">
<SidebarItem active>{/* <Icon /> Label */}</SidebarItem>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>{/* profile switcher */}</SidebarFooter>
</Sidebar>
)
}Anatomy#
Sidebar is a flex column on a bg-card surface. SidebarHeader and SidebarFooter pin to the top and bottom with a hairline separator, and either can hold a SidebarActions cluster beside its switcher; SidebarContent is the scroll region in between. Inside it, SidebarGroup is a navigation section. Give it a SidebarGroupLabel when it needs a heading, or leave it unlabeled for the primary pages. Each SidebarItem is a row.
Switchers#
SidebarSwitcher is one trigger used for both the workspace switcher (top) and the profile switcher (bottom): a leading visual, a title (with an optional subtitle), and an up/down caret. It renders a real <button>, so it drops straight into a DropdownMenuTrigger asChild. Radix owns the keyboard, focus, and ARIA; the switcher stays lit while its menu is open.
Full switcher#
SidebarSwitcher takes a variant. The default is the compact trigger (a tight gap, a small muted caret, and reduced padding), the everyday switcher, paired with a small leading (an Avatar size="xs", a single initial, or a compact logo tile). Pass variant="full" for the roomier identity row: a wider gap, a heavier title, and room for a subtitle. Pair it with a larger leading (an Avatar size="md", which keeps both initials) for a primary or standalone switcher.
Header actions#
SidebarActions puts icon buttons beside a switcher: search and notifications next to the workspace, the way GitBook and Squarespace head their rail. Drop it into SidebarHeader (or SidebarFooter, for help or settings) after the switcher, and the zone lays the two out as a row: the switcher hugs its name so the caret follows it, and the icons sit at the far edge, in line with the ends of the rows below. Fill it with ghost icon-only Buttons; they take the rail’s muted ink at rest and their aria-label as a tooltip.
This is where an app’s global tools live in the docked shell, which leaves the sheet’s header bar for the page. Collapsed to the icon rail, the buttons stack under the switcher’s mark instead of being cut off.
Items#
A SidebarItem is a <button> by default; pass asChild to wrap an <a> or a Next <Link>. Mark the current page with active (it fills the row and sets aria-current="page"). Lay the children out as <Icon /> Label and push a trailing Badge or count to the right with ml-auto.
Colored icon tiles#
Swap the bare leading glyph for a SidebarItemIcon: a soft, hue-tinted rounded square that gives each destination its own color, the “colored spaces” rail you see in Productboard or Linear. Wrap a single icon and pass a tone (decorative: purple, pink, teal, orange; status: green, blue, red, amber, plus brand). The tile scales with density and becomes the centered chip in the collapsed rail. The active row still reads through the neutral chip and brand bar, the tiles decorate, they don’t signal selection.
Nested navigation#
SidebarCollapsible folds a sub-list of routes under a parent row. Give it an icon and label, drop the child SidebarItems inside, and the row gains a caret that toggles them open, built on Radix Collapsible, so it owns the keyboard, focus, and height animation. The sub-list is indented under the label and traced by a guide rail; set active on the parent when one of its child routes is current. In the collapsed icon rail it falls back to a single icon row.
A long sub-list can be split under SidebarGroupLabel sub-headings: inside a sub-list the label stays a plain heading, even when the whole section sits in a collapsible group. With indicator on, folding a branch that holds the current route moves the pill to the first row still on show, so a parent marked active while folded catches it. The docs sidebar on this site is built exactly this way.
Collapsible sections#
Pass collapsible to a SidebarGroup and its SidebarGroupLabel becomes a toggle with a caret. Wrap the rows in SidebarGroupContent so they fold beneath it. Use defaultOpen (or the controlled open/onOpenChange pair) to set the initial state. Unlike nested navigation, this folds a whole flat section rather than revealing sub-routes.
Collapsed (icon rail)#
The rail has two variants, driven by the same markup. The default is the full-width rail; collapsed shrinks it to a 48px icon-only column. Labels go sr-only, each row becomes a centered icon, the active row is marked with a left accent bar instead of a full chip, sections are divided by a short rule, and the switcher reduces to its leading visual. Give every SidebarItem a label so the collapsed row gets a hover tooltip and an accessible name, and mark secondary sections with <SidebarGroup hideWhenCollapsed> so they drop out of the rail rather than reducing to bare glyphs.
Docked (in an app shell)#
A rail standing on its own is a card: it paints bg-card and a hairline against the content beside it. Inside an app shell it shouldn’t: the shell already owns the surface, and a second fill on top of it is the difference between a docked dashboard and a rail bolted onto one. So nesting a Sidebar in a LayoutSidebar turns docked on for you: no fill, no hairline, and the width comes from the shell column, which is also what animates the collapse.
The same applies in LayoutMobileSidebar, where the drawer panel is the surface. Pass docked explicitly to force the treatment outside a Layout, or docked={false} to opt a rail out of the shell it sits in. Combined with floating below, the card treatment wins: that pair is the detached rail floating in the shell gutter.
<Layout>
<LayoutSidebar>
<Sidebar aria-label="Main">…</Sidebar> {/* docked, no props needed */}
</LayoutSidebar>
<LayoutContent>…</LayoutContent>
</Layout>
{/* Standalone: keeps its own card surface and hairline */}
<Sidebar aria-label="Main">…</Sidebar>Floating (inset)#
floating detaches the rail into an inset card: a full outline, a rounded radius concentric with the app shell, and a shadow that lifts it off the canvas, instead of a single edge hairline. Pair it with padding on the container (or the Layout gutter) so it floats clear of the viewport edges.
Right side#
side="right" docks the rail against the right edge. The separating hairline flips to its left so it reads as an inspector/detail panel beside the main content. Everything else (switchers, sections, density) is symmetric.
Density#
density on Sidebar flows through context to the zone padding, the inter-section gap, the row height, and the switcher padding. compact is the default (app UI); comfortable is roomier. Set it once here or for a whole subtree with DensityProvider. See Density.