Sidebar Navigation Guide
The Sidebar navigation suite in packages/ui/src/Sidebar/ provides a comprehensive, multi-tiered sidebar navigation system for Waldur HomePort. Built on top of Radix UI primitives and styled with Tailwind CSS and design tokens, it provides responsive drawer behavior, collapsed icon rails with non-reflowing hover expansion, nested accordions with dynamic height measurement, and domain-specific workspace convenience components.
Architecture Overview
The Sidebar component system is structured into three distinct layers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Module File Map
All Sidebar components are co-located in packages/ui/src/Sidebar/:
| Module | Responsibility | Key Exports |
|---|---|---|
context.tsx |
Context definitions and hooks | useSidebar, useSidebarSubDepth, SidebarContextProps |
SidebarProvider.tsx |
State owner, media query, and CSS variables | SidebarProvider, SidebarProviderProps |
SidebarRoot.tsx |
Shell wrapper, desktop rail, mobile drawer, trigger | Sidebar, SidebarTrigger, SidebarInset |
SidebarLayout.tsx |
Structural layout slots and Radix ScrollArea | SidebarHeader, SidebarContent, SidebarFooter, SidebarGroup, SidebarMenu, SidebarMenuItem, SidebarSeparator |
SidebarMenuButton.tsx |
Interactive button primitive, tooltips, badge | SidebarMenuButton, SidebarMenuBadge, sidebarMenuButtonVariants |
SidebarMenuSub.tsx |
Nested accordions and height-measured Collapsible | SidebarMenuSub, SidebarMenuSubItem, SidebarMenuSubButton, SidebarMenuAccordion, SidebarMenuSeparator |
SidebarMenuTree.tsx |
Recursive tree rendering, overflow pagination | SidebarMenuTree, useExclusiveOpen, SidebarMenuTreeItem |
SidebarConvenience.tsx |
Dashboard mockup convenience wrappers | SidebarBrand, SidebarModeCard, SidebarNavItem, SidebarMenuLinkItem, SidebarCallToAction, SidebarSection |
SidebarToggleGraphic.tsx |
Canonical SVG glyph for collapse button | SidebarToggleGraphic |
index.ts |
Public export surface | All above exports re-exported cleanly |
State Management & Hooks
useSidebar()
The primary hook consumed by sidebar components and page shells to inspect and control the sidebar state. Must be rendered within a <SidebarProvider>.
1 2 3 4 5 6 7 8 9 10 11 | |
useSidebarSubDepth()
Hierarchical navigation tracks nesting depth through SidebarMenuSubDepthContext. Top-level menu items have a depth of 0. Each <SidebarMenuSub> automatically increments this depth:
depth = 0: Top-level items (no indentation, 6px border-radius).depth = 1: First nested submenu level (indented, square corners, subitem active background).depth = 2+: Deeper nested submenus (incremented indentation, second-level subitem background).
1 2 3 4 5 6 7 | |
useExclusiveOpen(initial?: string)
A state hook for managing mutually exclusive accordions among sibling items (e.g., ensuring opening one accordion automatically closes all others at the same level):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Responsive Behavior & Layout Modes
1. Desktop Expanded vs. Collapsed Rail
On viewports above mobileBreakpoint (default 768px):
- Expanded Width:
--sidebar-width(300px). Full logo, section labels, badges, item labels, and carets are displayed. - Collapsed Rail Width:
--sidebar-width-icon(3rem/48px). Labels and badges hide; menu items shrink to centered icon buttons (size-9/ 36px);SidebarModeCardcollapses to an icon tile;SidebarBranddisplays the compact mobile logo mark (swapping from the full wordmark), with the collapse toggle revealed on hover.
2. Desktop Hover Expansion (No Page Reflow)
When the desktop sidebar is collapsed in 'icon' mode, hovering the mouse over the rail temporarily expands the panel back to 300px without pushing or reflowing adjacent page content:
1 2 3 4 5 6 7 | |
Technical Implementation:
- An invisible Layout Spacer (
<div>) sits in the flex layout alongsideSidebarInset. Its width is strictly tied to the canonicalstate('expanded'or'collapsed'). - The visible Panel Overlay (
<div className="group/panel fixed inset-y-0 z-sidebar-panel">) sits atz-index: 105. - Hovering the panel triggers
onMouseEnter, settingisHoverExpanded = true. The fixed panel widens to300pxand adds a subtle drop shadow (md:shadow-[5px_0px_10px_rgba(70,78,95,0.075)]), floating gracefully over the main layout without causing layout thrashing. - Item label tooltips are automatically suppressed while
isHoverExpandedis true, since the real text label is already visible.
3. Mobile Sheet Drawer
When the viewport width is below mobileBreakpoint:
- The desktop rail and spacer are hidden (
hidden md:block). - The sidebar renders inside a Radix
Sheetmodal drawer anchored to the configuredside(leftorright). - Drawer width is pinned to
250px. - Backdrop overlay dims the rest of the application.
openMobileandsetOpenMobilecontrol visibility. CallingtoggleSidebar()automatically switches between toggling mobile drawer vs. desktop collapse.
4. Collapsible Modes
<Sidebar collapsible="..."> supports three modes:
| Mode | Behavior |
|---|---|
'icon' |
Collapses to a 3rem (48px) icon-only rail with hover-expansion. (Default) |
'offcanvas' |
Slides completely offscreen when collapsed. |
'none' |
Static fixed sidebar that cannot be collapsed. |
Theming & Design Tokens
Sidebar styling is entirely powered by CSS custom properties defined in @waldur/design-tokens/src/sidebarColors.css.
Token Reference
| Token | Description |
|---|---|
--surface-sidebar-bg |
Background color of the sidebar panel |
--surface-sidebar-border |
Border color between the sidebar and main content |
--nav-item-text |
Color of navigation item labels and text |
--nav-item-icon |
Color of navigation item icons and caret glyphs |
--nav-item-hover-bg |
Background color on row hover |
--nav-item-active-bg |
Background color for active navigation rows |
--nav-accordion-active |
Ambient background of an open top-level accordion |
--nav-sub-accordion-active |
Ambient background of an open nested accordion |
--nav-item-subitem-bg |
Active background color for level-1 nested items |
--nav-item-subitem2-bg |
Active background color for level-2+ nested items |
--nav-badge-text |
Text color for item count badges |
--nav-badge-border |
Border color for item count badges |
--nav-separator |
Color of horizontal dividing lines |
--nav-scrollbar-color |
Scrollbar thumb color in SidebarContent |
--nav-scrollbar-hover-color |
Scrollbar thumb hover color |
Sidebar Style Variants
Waldur supports 5 distinct sidebar color styles via the data-sidebar-style attribute:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Component API Reference
Core Primitives
<SidebarProvider>
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen |
boolean |
true |
Uncontrolled default desktop open state |
open |
boolean |
undefined |
Controlled desktop open state |
onOpenChange |
(open: boolean) => void |
undefined |
Callback fired on desktop open change |
mobileBreakpoint |
number |
768 |
Viewport breakpoint (px) for mobile drawer mode |
renderWrapper |
boolean |
true |
Set false to omit outer <div> and bind vars to <html> |
<Sidebar>
| Prop | Type | Default | Description |
|---|---|---|---|
side |
'left' \| 'right' |
'left' |
Edge of the viewport to anchor the sidebar |
collapsible |
'icon' \| 'offcanvas' \| 'none' |
'icon' |
Desktop collapse behavior |
className |
string |
undefined |
Additional classes for the sidebar panel |
<SidebarTrigger>
| Prop | Type | Default | Description |
|---|---|---|---|
icon |
ReactNode |
<SidebarSimpleIcon /> |
Custom toggle icon glyph |
className |
string |
undefined |
Additional classes |
<SidebarInset>
Container for the main application content rendered adjacent to <Sidebar>. Applies min-w-0 flex-1 flex-col to ensure proper flex shrinking and avoid page-level horizontal overflow.
<SidebarContent>
Scrollable container wrapping navigation menus. Built on Radix ScrollArea with an accessible custom scrollbar.
- Default
type="hover"(scrollbar fades in on hover and hides when idle). - Scrollbar is automatically suppressed in collapsed icon rail mode.
Menu & Accordion Primitives
<SidebarMenuButton>
Interactive button row for navigation items.
| Prop | Type | Default | Description |
|---|---|---|---|
active |
boolean |
false |
Highlights row with --nav-item-active-bg |
asChild |
boolean |
false |
Delegates rendering to child element via Radix Slot |
tooltip |
string |
undefined |
Tooltip displayed in collapsed icon rail mode |
disabled |
boolean |
false |
Disables interaction and dims opacity to 50% |
disabledTooltip |
string |
undefined |
Tooltip explaining why the item is disabled |
<SidebarMenuBadge>
Pill badge component for counts and status indicators. Automatically hides in collapsed icon rail mode.
<SidebarMenuAccordion>
Collapsible menu group featuring an animated slide transition driven by ResizeObserver.
| Prop | Type | Default | Description |
|---|---|---|---|
title |
ReactNode |
required | Header label or element |
icon |
ReactNode |
undefined |
Leading icon |
badge |
ReactNode |
undefined |
Trailing badge or indicator before caret |
open |
boolean |
undefined |
Controlled open state |
onOpenChange |
(open: boolean) => void |
undefined |
Callback fired on open change |
disabled |
boolean |
false |
Disables accordion toggle |
disabledTooltip |
string |
undefined |
Explains why accordion is disabled |
<SidebarMenuTree>
Recursive menu tree renderer for hierarchical categories (such as cloud resource menus).
| Prop | Type | Default | Description |
|---|---|---|---|
items |
SidebarMenuTreeItem[] |
required | Tree data structure |
renderItem |
(item: SidebarMenuTreeItem) => ReactNode |
required | Renderer for leaf nodes |
maxVisibleItems |
number |
undefined |
Truncation limit before "Show more" toggle |
moreLabel |
(hidden: number) => ReactNode |
"Show N more" |
Label for the expand toggle |
lessLabel |
ReactNode |
"Show less" |
Label for the collapse toggle |
moreTooltip |
(hidden: number) => ReactNode |
undefined |
Tooltip for the collapsed toggle row |
openId |
string |
undefined |
Controlled open accordion ID |
onToggle |
(id: string) => (open: boolean) => void |
undefined |
Controlled toggle handler |
Convenience Wrappers
SidebarBrand: Header row with a quick shortcuts launcher, centered logo, and collapse toggle.SidebarModeCard: Workspace context card (Organization, Project, Admin) with mode switcher affordance.SidebarCallToAction: Outlined primary action button (e.g. "+ Add resource") placed at the top of menu lists.SidebarNavItem: Standard navigation item with optionalhreforonClick.SidebarMenuLinkItem: Router-agnostic link wrapper for integration with routing libraries.SidebarSection: Pre-composed section wrappingSidebarGroup,SidebarGroupLabel, andSidebarMenu.SidebarToggleGraphic: Canonical SVG glyph for the collapse button.
Implementation Recipes
1. Minimal Responsive Sidebar
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | |
2. Nested Accordion Menus
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | |
3. Recursive Tree with Truncation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | |
4. Router-Integrated Links (SidebarMenuLinkItem)
When integrating with routing frameworks (such as UI-Router, React Router, or TanStack Router):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
5. Headless Provider for Custom Page Shells
When the outer application shell manages its own layout containers (e.g. fixed navigation wrappers):
1 2 3 4 5 6 7 8 9 10 | |
Accessibility (a11y) & UX Considerations
- Semantic HTML Elements:
- Navigation is structured using
<nav>,<ul>,<li>, and<button>. - Menus utilize
role="separator"on horizontal dividers. -
Screen-reader-only headings (
<SheetHeader className="sr-only">) satisfy dialog title accessibility requirements in the mobile Sheet. -
Keyboard Navigation & Focus Management:
SidebarTriggerreflects its state viaaria-pressed={state === 'collapsed'}and provides cleararia-label="Toggle sidebar".-
Collapsed desktop triggers reveal on
:focus-visible, allowing full keyboard controllability even when hidden visually in hover mode. -
Dual Tooltip Strategy:
- Collapsed Tooltip:
<SidebarMenuButton tooltip="...">shows an icon explanation on the right side only when the sidebar is collapsed into the icon rail. It automatically suppresses when expanded or hover-expanded to avoid redundant tooltips. - Disabled Tooltip:
<SidebarMenuButton disabled disabledTooltip="...">explains why an action cannot be performed, and appears unconditionally when disabled regardless of whether the sidebar is collapsed or expanded. Because disabled buttons and elements withpointer-events: nonesuppress browser pointer events and keyboard focus, the trigger is wrapped in an interactive container (<span tabIndex={0} className="... cursor-not-allowed ...">) ensuring the tooltip reliably opens on both pointer hover and keyboard focus across all browsers.