Skip to content

Sidebar

A vertical navigation panel typically positioned on the left side of the layout.

  • Side Navigation
  • Side Panel

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

VariantDescriptionUse 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 + FlyoutCollapsed icon rail with flyout sub-menus on hover.Complex navigation with nested items in compact mode.
Overlay / DrawerSidebar overlays content on small screens, dismissed via backdrop click or swipe.Mobile and tablet breakpoints. See Drawer.
PersistentAlways visible; content area adjusts its width.Desktop apps where navigation must never be hidden.
TemporaryHidden by default; toggled via hamburger button.Mobile-first layouts, content-focused apps.
Inset / ContainedSidebar sits inside the content area rather than at viewport edge.Documentation side-navigation, settings panels.

Layout Interaction

Screen WidthRecommended Behavior
≥ 1280pxExpanded sidebar, full labels.
1024–1279pxCollapsed icon rail (user can expand).
< 1024pxHidden 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

PropertyTypeDefaultDescription
collapsedbooleanfalseControls collapsed (icon-only) vs expanded (icon + label) state.
onCollapsedChange(collapsed: boolean) => void—Callback when collapse state changes.
widthnumber | string256Expanded width in pixels or CSS value.
collapsedWidthnumber | string64Width when collapsed.
itemsSidebarItem[]—Navigation items with optional nesting.
headerReactNode—Content rendered at the top (logo, workspace switcher).
footerReactNode—Content rendered at the bottom (user profile, settings).
activeItemstring—Currently active item identifier.
onNavigate(href: string) => void—Navigation callback for SPA routing.
breakpointnumber1024Viewport width below which sidebar becomes an overlay.

SidebarItem Shape

PropertyTypeDescription
idstringUnique identifier
labelstringDisplay text
hrefstringNavigation URL
iconReactNodeLeading icon (required for collapsed state)
badgestring | numberOptional notification badge
childrenSidebarItem[]Nested sub-items (creates a collapsible group)
sectionstringGroup heading label (renders as a non-interactive section divider)

Token Mappings

Design Token Mappings

TokenRoleTypical Value
--sidebar-widthExpanded width256px
--sidebar-width-collapsedCollapsed width64px
--sidebar-bgBackground colorvar(--color-surface-secondary)
--sidebar-border-colorRight bordervar(--color-border-subtle)
--sidebar-item-heightIndividual item height40px
--sidebar-item-padding-xHorizontal item paddingvar(--space-3, 0.75rem)
--sidebar-item-radiusItem border-radiusvar(--radius-md, 6px)
--sidebar-item-colorDefault item text colorvar(--color-text-secondary)
--sidebar-item-color-hoverHover text colorvar(--color-text-primary)
--sidebar-item-bg-hoverHover backgroundvar(--color-surface-hover)
--sidebar-item-color-activeActive item text colorvar(--color-brand)
--sidebar-item-bg-activeActive item backgroundvar(--color-brand-subtle)
--sidebar-section-label-sizeSection heading font size0.6875rem (11px)
--sidebar-transition-durationCollapse/expand animation200ms

Use the Spacing Calculator to ensure --sidebar-item-padding-x and internal gaps align with your global spacing scale.

States

Sidebar States

StateVisual TreatmentBehavior
ExpandedFull width with icons and text labels. Section headings visible.Default desktop state.
CollapsedIcon rail only. Labels hidden.Icons must remain recognizable. Active indicator via background or left-border accent.
Item DefaultSecondary text color, transparent background.Clickable.
Item HoverBackground shifts to hover surface color, text to primary color.Cursor: pointer.
Item FocusVisible focus ring (2px outline or equivalent).Keyboard-navigable.
Item ActiveBrand-colored text and subtle brand background. Optional left border accent (3px).Indicates current page.
Item with BadgeNotification count or dot rendered on the right side of the item.Badge remains visible in collapsed state, positioned over the icon.
Sub-menu ExpandedChild items rendered below parent with indentation (12–16px). Chevron rotates.Clicking parent toggles children visibility.
Sub-menu CollapsedChildren 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 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).
  • 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 and aria-controls pointing to the sub-menu id (WCAG 4.1.2 Name, Role, Value).
  • The collapse/expand toggle button needs aria-label="Collapse sidebar" / "Expand sidebar" (or use aria-expanded on the toggle itself).

Keyboard Navigation:

  • All items must be reachable via 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).
  • 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-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.

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

html
<!-- 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>
tsx
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.

NavigationSide NavigationSide Panel