Overview
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:
- Applications with 5+ primary navigation destinations
- Dashboards and admin panels with grouped sections (Analytics, Settings, Users, etc.)
- Tools requiring persistent navigation alongside content (email clients, project management, IDEs)
- Multi-level navigation with collapsible sub-menus
- Applications where context switching is frequent
When NOT to use a Sidebar:
- Marketing websites or content pages — use a Navigation Bar instead
- Mobile-first applications — sidebars must transform into Drawers or bottom tabs on small screens
- Apps with fewer than 4 navigation items — a horizontal nav is simpler and wastes less space
- Read-heavy content layouts where maximum content width matters (blogs, articles)
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.
Variants
Sidebar Variants
| 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. |
Layout Interaction
| 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.
Properties
Sidebar Properties
| 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. |
SidebarItem Shape
| 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 Mappings
Design Token Mappings
| 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.
States
Sidebar States
| 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. |
Accessibility
Accessibility
Sidebar navigation is essentially a landmarks-and-lists problem. When done right, screen-reader users can discover and traverse it efficiently.
ARIA & Semantics:
- Wrap the sidebar in a
<nav>element with a distinctaria-label(e.g.,aria-label="Main navigation") to differentiate it from other navigation landmarks like breadcrumbs (WCAG 1.3.1 Info and Relationships). - Render navigation items as an
<ul>with<li>children. Nested sub-menus are nested<ul>elements inside their parent<li>. - Mark the active item with
aria-current="page"(WCAG 1.3.1). This tells screen readers which navigation item corresponds to the current page. - Collapsible groups should use
aria-expanded="true|false"on the parent button andaria-controlspointing to the sub-menuid(WCAG 4.1.2 Name, Role, Value). - The collapse/expand toggle button needs
aria-label="Collapse sidebar"/"Expand sidebar"(or usearia-expandedon the toggle itself).
Keyboard Navigation:
- All items must be reachable via
Tab(or arrow keys if implementingrole="tree"pattern — see below). EnterorSpaceactivates a navigation item or toggles a collapsible group.Escapecloses the sidebar when it's in overlay/drawer mode (WCAG 2.1.1 Keyboard).- When the sidebar opens as an overlay, focus must move into the sidebar and trap until dismissed (focus trapping), then return to the trigger element.
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:
- Active and hover states must maintain at least 4.5:1 contrast for text against their background (WCAG 1.4.3 Contrast Minimum). Verify with the Contrast Checker.
- Focus indicators must meet 3:1 contrast against adjacent colors (WCAG 2.4.7 Focus Visible).
- In collapsed mode, icon-only items must have accessible names — either via
aria-labelon 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.
Usage Guidelines
Usage Guidelines
Do:
- Group related items under section headings (e.g., "Analytics", "Settings", "Account")
- Use meaningful icons that are recognizable even without labels — they become the sole signifier in collapsed mode
- Persist the collapsed/expanded preference (localStorage or user settings)
- Animate the collapse/expand transition (200–300ms ease) for spatial continuity
- Place the most-used items near the top; relegate settings and profile to the footer
- Show notification badges for items that need attention (unread messages, pending approvals)
- Use the Spacing Calculator to align item padding with your global spacing scale
Don't:
- Don't nest more than 2 levels deep — if your IA requires 3+ levels, consider restructuring
- Don't auto-collapse on desktop without remembering the user's preference
- Don't use hover-to-expand as the only way to access sub-menus — hover is unavailable on touch devices
- Don't put critical actions only in the sidebar footer where they may be scrolled out of view
- Don't use different icon styles (outlined vs filled) within the same sidebar — maintain visual consistency
- Don't make the sidebar wider than 320px — it starts consuming too much content area
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.
Code Snippets
<!-- 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)}
/>Design Systems
Design System Implementations
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.