Loading…
Loading…
A vertical navigation panel typically positioned on the left side of the layout.
The Sidebar is a vertical navigation panel — typically anchored to the left edge of the viewport — that provides persistent access to top-level sections, tools, or workflows. It is the structural backbone of dashboards, admin panels, email clients, and any application where users need constant access to multiple destinations without losing context.
Unlike a Navigation Bar that compresses navigation into a horizontal strip, the sidebar exploits vertical space, which is abundant on desktop screens. This makes it ideal for applications with 5–30+ navigation items organized into groups and sub-sections. The sidebar also serves as a spatial anchor — users know the left edge always contains navigation, creating a reliable mental model.
When to use a Sidebar:
When NOT to use a Sidebar:
Sidebar widths interact directly with your layout grid. Use the Spacing Calculator to establish consistent internal padding, and leverage the Responsive Design Tool to plan collapse breakpoints.
| Variant | Description | Use Case |
|---|---|---|
| Expanded (Full) | Sidebar displays icons and text labels. Typical width: 240–280px. | Default desktop state. Dashboards with sufficient screen width. |
| Collapsed (Icon-only) | Sidebar shrinks to icon rail (56–72px). Labels hidden, revealed via Tooltips on hover. | User-initiated collapse to maximize content area. |
| Mini + Flyout | Collapsed icon rail with flyout sub-menus on hover. | Complex navigation with nested items in compact mode. |
| Overlay / Drawer | Sidebar overlays content on small screens, dismissed via backdrop click or swipe. | Mobile and tablet breakpoints. See Drawer. |
| Persistent | Always visible; content area adjusts its width. | Desktop apps where navigation must never be hidden. |
| Temporary | Hidden by default; toggled via hamburger button. | Mobile-first layouts, content-focused apps. |
| Inset / Contained | Sidebar sits inside the content area rather than at viewport edge. | Documentation side-navigation, settings panels. |
| Screen Width | Recommended Behavior |
|---|---|
| ≥ 1280px | Expanded sidebar, full labels. |
| 1024–1279px | Collapsed icon rail (user can expand). |
| < 1024px | Hidden by default, opens as overlay Drawer. |
Design responsive sidebar breakpoints with the Responsive Design Tool. The transition between expanded and collapsed states should use a smooth width animation (200–300ms ease) rather than abrupt jumps.
| Property | Type | Default | Description |
|---|---|---|---|
collapsed | boolean | false | Controls collapsed (icon-only) vs expanded (icon + label) state. |
onCollapsedChange | (collapsed: boolean) => void | — | Callback when collapse state changes. |
width | number | string | 256 | Expanded width in pixels or CSS value. |
collapsedWidth | number | string | 64 | Width when collapsed. |
items | SidebarItem[] | — | Navigation items with optional nesting. |
header | ReactNode | — | Content rendered at the top (logo, workspace switcher). |
footer | ReactNode | — | Content rendered at the bottom (user profile, settings). |
activeItem | string | — | Currently active item identifier. |
onNavigate | (href: string) => void | — | Navigation callback for SPA routing. |
breakpoint | number | 1024 | Viewport width below which sidebar becomes an overlay. |
| Property | Type | Description |
|---|---|---|
id | string | Unique identifier |
label | string | Display text |
href | string | Navigation URL |
icon | ReactNode | Leading icon (required for collapsed state) |
badge | string | number | Optional notification badge |
children | SidebarItem[] | Nested sub-items (creates a collapsible group) |
section | string | Group heading label (renders as a non-interactive section divider) |
| Token | Role | Typical Value |
|---|---|---|
--sidebar-width | Expanded width | 256px |
--sidebar-width-collapsed | Collapsed width | 64px |
--sidebar-bg | Background color | var(--color-surface-secondary) |
--sidebar-border-color | Right border | var(--color-border-subtle) |
--sidebar-item-height | Individual item height | 40px |
--sidebar-item-padding-x | Horizontal item padding | var(--space-3, 0.75rem) |
--sidebar-item-radius | Item border-radius | var(--radius-md, 6px) |
--sidebar-item-color | Default item text color | var(--color-text-secondary) |
--sidebar-item-color-hover | Hover text color | var(--color-text-primary) |
--sidebar-item-bg-hover | Hover background | var(--color-surface-hover) |
--sidebar-item-color-active | Active item text color | var(--color-brand) |
--sidebar-item-bg-active | Active item background | var(--color-brand-subtle) |
--sidebar-section-label-size | Section heading font size | 0.6875rem (11px) |
--sidebar-transition-duration | Collapse/expand animation | 200ms |
Use the Spacing Calculator to ensure --sidebar-item-padding-x and internal gaps align with your global spacing scale.
| State | Visual Treatment | Behavior |
|---|---|---|
| Expanded | Full width with icons and text labels. Section headings visible. | Default desktop state. |
| Collapsed | Icon rail only. Labels hidden. | Icons must remain recognizable. Active indicator via background or left-border accent. |
| Item Default | Secondary text color, transparent background. | Clickable. |
| Item Hover | Background shifts to hover surface color, text to primary color. | Cursor: pointer. |
| Item Focus | Visible focus ring (2px outline or equivalent). | Keyboard-navigable. |
| Item Active | Brand-colored text and subtle brand background. Optional left border accent (3px). | Indicates current page. |
| Item with Badge | Notification count or dot rendered on the right side of the item. | Badge remains visible in collapsed state, positioned over the icon. |
| Sub-menu Expanded | Child items rendered below parent with indentation (12–16px). Chevron rotates. | Clicking parent toggles children visibility. |
| Sub-menu Collapsed | Children hidden. Parent shows a chevron indicator. | In collapsed sidebar, sub-menus appear as flyout panels on hover. |
| Overlay (Mobile) | Full sidebar slides in from left edge with a backdrop overlay. | Dismissed via backdrop click, swipe-left gesture, or Escape key. |
Sidebar navigation is essentially a landmarks-and-lists problem. When done right, screen-reader users can discover and traverse it efficiently.
ARIA & Semantics:
<nav> element with a distinct aria-label (e.g., aria-label="Main navigation") to differentiate it from other navigation landmarks like breadcrumbs (WCAG 1.3.1 Info and Relationships).<ul> with <li> children. Nested sub-menus are nested <ul> elements inside their parent <li>.aria-current="page" (WCAG 1.3.1). This tells screen readers which navigation item corresponds to the current page.aria-expanded="true|false" on the parent button and aria-controls pointing to the sub-menu id (WCAG 4.1.2 Name, Role, Value).aria-label="Collapse sidebar" / "Expand sidebar" (or use aria-expanded on the toggle itself).Keyboard Navigation:
Tab (or arrow keys if implementing role="tree" pattern — see below).Enter or Space activates a navigation item or toggles a collapsible group.Escape closes the sidebar when it's in overlay/drawer mode (WCAG 2.1.1 Keyboard).Tree View Pattern (Optional):
For deeply nested sidebars, the WAI-ARIA tree view pattern (role="tree", role="treeitem", role="group") may be appropriate. This enables arrow-key navigation: Up/Down to move between items, Left/Right to collapse/expand groups. However, this pattern is complex to implement correctly and is overkill for flat or two-level navigation.
Contrast & Visibility:
aria-label on the link or a visually-hidden <span> containing the label text.Responsive Considerations:
When the sidebar transforms into an overlay at mobile breakpoints, it must behave as a modal Drawer: focus trapping, Escape to close, backdrop click to dismiss. The toggle button (hamburger) should have aria-expanded reflecting the drawer's open state. Plan breakpoints with the Responsive Design Tool.
Do:
Don't:
Responsive Strategy: The standard pattern: expanded sidebar at ≥1280px, collapsed at 1024–1279px, overlay drawer at <1024px. However, tune these breakpoints to your application's content needs. Use the Responsive Design Tool to preview behavior at each breakpoint. The transition from sidebar to drawer should preserve the same navigation items and hierarchy — only the container behavior changes.
<!-- Sidebar – Semantic HTML -->
<nav class="sidebar" aria-label="Main navigation">
<div class="sidebar-header">
<img src="/logo.svg" alt="App Logo" width="32" height="32" />
<span class="sidebar-title">Dashboard</span>
</div>
<ul class="sidebar-nav">
<li class="sidebar-section-label">Analytics</li>
<li>
<a href="/dashboard" class="sidebar-item active" aria-current="page">
<svg class="sidebar-icon" aria-hidden="true"><!-- chart icon --></svg>
<span class="sidebar-label">Overview</span>
</a>
</li>
<li>
<a href="/reports" class="sidebar-item">
<svg class="sidebar-icon" aria-hidden="true"><!-- report icon --></svg>
<span class="sidebar-label">Reports</span>
</a>
</li>
<li class="sidebar-section-label">Management</li>
<li>
<button class="sidebar-item" aria-expanded="false" aria-controls="sub-users">
<svg class="sidebar-icon" aria-hidden="true"><!-- users icon --></svg>
<span class="sidebar-label">Users</span>
<svg class="sidebar-chevron" aria-hidden="true"><!-- chevron --></svg>
</button>
<ul id="sub-users" class="sidebar-submenu" hidden>
<li><a href="/users/all" class="sidebar-item">All Users</a></li>
<li><a href="/users/roles" class="sidebar-item">Roles</a></li>
</ul>
</li>
</ul>
<div class="sidebar-footer">
<a href="/settings" class="sidebar-item">
<svg class="sidebar-icon" aria-hidden="true"><!-- settings icon --></svg>
<span class="sidebar-label">Settings</span>
</a>
</div>
</nav>
<style>
.sidebar {
width: var(--sidebar-width, 256px);
height: 100vh;
position: fixed;
top: 0;
left: 0;
background: var(--sidebar-bg, #f9fafb);
border-right: 1px solid var(--sidebar-border-color, #e5e7eb);
display: flex;
flex-direction: column;
transition: width var(--sidebar-transition-duration, 200ms) ease;
overflow: hidden;
}
.sidebar.collapsed {
width: var(--sidebar-width-collapsed, 64px);
}
.sidebar.collapsed .sidebar-label,
.sidebar.collapsed .sidebar-title,
.sidebar.collapsed .sidebar-section-label,
.sidebar.collapsed .sidebar-chevron {
display: none;
}
.sidebar-nav {
list-style: none;
padding: 0.5rem;
margin: 0;
flex: 1;
overflow-y: auto;
}
.sidebar-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem var(--sidebar-item-padding-x, 0.75rem);
border-radius: var(--sidebar-item-radius, 6px);
color: var(--sidebar-item-color, #6b7280);
text-decoration: none;
cursor: pointer;
border: none;
background: none;
width: 100%;
font: inherit;
height: var(--sidebar-item-height, 40px);
}
.sidebar-item:hover {
background: var(--sidebar-item-bg-hover, #f3f4f6);
color: var(--sidebar-item-color-hover, #111827);
}
.sidebar-item.active {
background: var(--sidebar-item-bg-active, #eff6ff);
color: var(--sidebar-item-color-active, #2563eb);
font-weight: 600;
}
.sidebar-submenu {
list-style: none;
padding-left: 1rem;
margin: 0;
}
</style>interface SidebarItem {
id: string;
label: string;
href?: string;
icon: React.ReactNode;
badge?: string | number;
children?: SidebarItem[];
}
interface SidebarProps {
items: SidebarItem[];
activeItem?: string;
collapsed?: boolean;
onCollapsedChange?: (collapsed: boolean) => void;
onNavigate?: (href: string) => void;
header?: React.ReactNode;
footer?: React.ReactNode;
}
function Sidebar({ items, activeItem, collapsed = false, onCollapsedChange, onNavigate, header, footer }: SidebarProps) {
const [expandedGroups, setExpandedGroups] = React.useState<Set<string>>(new Set());
const toggleGroup = (id: string) => {
setExpandedGroups((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
};
const renderItem = (item: SidebarItem, depth = 0) => {
const isActive = item.id === activeItem;
const hasChildren = item.children && item.children.length > 0;
const isExpanded = expandedGroups.has(item.id);
if (hasChildren) {
return (
<li key={item.id}>
<button
className={`sidebar-item ${isActive ? "active" : ""}`}
onClick={() => toggleGroup(item.id)}
aria-expanded={isExpanded}
aria-controls={`sub-${item.id}`}
style={{ paddingLeft: `${0.75 + depth * 1}rem` }}
>
{item.icon}
{!collapsed && <span className="sidebar-label">{item.label}</span>}
{!collapsed && <span className={`sidebar-chevron ${isExpanded ? "rotated" : ""}`}>›</span>}
</button>
{isExpanded && (
<ul id={`sub-${item.id}`} className="sidebar-submenu">
{item.children!.map((child) => renderItem(child, depth + 1))}
</ul>
)}
</li>
);
}
return (
<li key={item.id}>
<a
href={item.href}
className={`sidebar-item ${isActive ? "active" : ""}`}
aria-current={isActive ? "page" : undefined}
onClick={(e) => {
if (onNavigate && item.href) {
e.preventDefault();
onNavigate(item.href);
}
}}
style={{ paddingLeft: `${0.75 + depth * 1}rem` }}
>
{item.icon}
{!collapsed && <span className="sidebar-label">{item.label}</span>}
{item.badge != null && <span className="sidebar-badge">{item.badge}</span>}
</a>
</li>
);
};
return (
<nav className={`sidebar ${collapsed ? "collapsed" : ""}`} aria-label="Main navigation">
{header && <div className="sidebar-header">{header}</div>}
<ul className="sidebar-nav">{items.map((item) => renderItem(item))}</ul>
{footer && <div className="sidebar-footer">{footer}</div>}
<button
className="sidebar-collapse-toggle"
onClick={() => onCollapsedChange?.(!collapsed)}
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
>
{collapsed ? "»" : "«"}
</button>
</nav>
);
}
// Usage
<Sidebar
collapsed={false}
onCollapsedChange={setCollapsed}
activeItem="overview"
header={<Logo />}
footer={<UserMenu />}
items={[
{ id: "overview", label: "Overview", href: "/dashboard", icon: <ChartIcon /> },
{ id: "reports", label: "Reports", href: "/reports", icon: <ReportIcon /> },
{
id: "users",
label: "Users",
icon: <UsersIcon />,
children: [
{ id: "all-users", label: "All Users", href: "/users/all", icon: <ListIcon /> },
{ id: "roles", label: "Roles", href: "/users/roles", icon: <ShieldIcon /> },
],
},
]}
onNavigate={(href) => router.push(href)}
/>Material Design (MUI) implements sidebar navigation via the <Drawer> component with variant="permanent" for persistent sidebars and variant="temporary" for mobile overlays. Combined with <List>, <ListItem>, <ListItemButton>, and <Collapse> for nested groups, it forms a complete sidebar pattern. MUI's mini variant drawer pattern provides the collapsed icon-rail behavior. The responsive variant switches between permanent and temporary based on breakpoints.
Ant Design provides a <Layout.Sider> component with built-in collapsible support, collapsed state management, and breakpoint-based auto-collapse (breakpoint="lg"). Paired with <Menu> component for items, it supports nested sub-menus (<Menu.SubMenu>), item groups, and inline/vertical modes. The collapsed state automatically switches to icon-only display with popup sub-menus.
Chakra UI does not ship a dedicated sidebar component but provides the building blocks: <Drawer> for overlay behavior and <Box> / <VStack> for the persistent panel. The Chakra Pro templates include a full sidebar pattern with nav items, section headings, and collapse toggle. Developers compose these primitives with useDisclosure for state management.
Bootstrap offers an offcanvas component for overlay sidebars and CSS utility classes for persistent sidebar layouts. The Bootstrap sidebar pattern typically combines .d-flex, .flex-column, and list group components. No built-in collapse-to-icon-rail behavior — this requires custom CSS.
Apple Human Interface Guidelines prescribes a sidebar for macOS apps using NavigationSplitView (SwiftUI) or NSSplitViewController. The sidebar column is typically 200–260pt wide, supports section grouping, and collapses to icons in compact layouts. On iPadOS, the sidebar adapts between persistent and overlay modes based on size class.
Shadcn/ui recently added a dedicated <Sidebar> component — a composable system with <SidebarProvider>, <SidebarTrigger>, <SidebarContent>, <SidebarGroup>, <SidebarMenu>, and <SidebarMenuItem>. It supports collapsible groups, icon-only mode, and mobile overlay via sheet. Built on Radix primitives with full keyboard navigation and ARIA support.