Helix adoption: When building a new product, always use the Helix master sidebar pattern described below — do not build from raw shadcn/ui
<Sidebar>primitives. The master defines the required anatomy, sizing, and styling. Products supply their own nav items, icons, and optional features.
Import
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarTrigger,
} from "@timelycare/helix-ui"
Props
interface SidebarProps {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
className?: string
}
interface SidebarMenuButtonProps {
asChild?: boolean
isActive?: boolean
tooltip?: string | TooltipContentProps
variant?: "default" | "outline"
size?: "default" | "sm" | "lg"
}
Structure
| Part | Tailwind |
|---|---|
| Sidebar (expanded) | w-[248px] bg-sidebar border-r border-sidebar-border |
| Sidebar (collapsed) | w-12 (48px, icon-only rail) |
| SidebarHeader | h-16 px-6 py-3 border-b border-sidebar-border |
| SidebarContent | flex-1 overflow-y-auto p-2 |
| SidebarFooter | p-2 — natural rendered height with a single LogOut button: 48px (8px top + 32px button + 8px bottom). When used alongside a page-level footer bar, override padding to py-4 to match the bar's h-16 (64px). |
| SidebarGroup | p-2 |
| SidebarMenu | flex flex-col |
Note: The TimelyCare sidebar uses a flat navigation without
SidebarGroupLabelheadings. All menu items sit in a singleSidebarGroupwith no subheadings.SidebarGroupLabelis available but not used in any current product sidebar.
Master Sidebar (Use This)
The master sidebar is the required starting point for every Helix product. It defines the structural scaffold and styling rules — not a fixed set of menu items. Do not build sidebars from raw shadcn/ui primitives; use this pattern so that every product gets consistent sizing, spacing, states, and footer behavior out of the box.
Non-negotiable aspects
- Menu item sizing and styles — Every menu item uses
size-4(16px) icons,text-sm font-medium,h-10 gap-2 px-2 rounded-mdspacing (40px fixed height), and the active/hover state tokens listed in the States section. In collapsed mode, each item renders as asize-10(40×40px) square button with the icon centered. The expandedh-10and collapsedsize-10ensure icons stay at the same vertical position during the expand/collapse transition. - Log Out button in the footer — Every Helix sidebar places a visible
LogOuticon + text button inSidebarFooter. Do not use the default shadcn avatar/dropdown menu pattern. - Optional notification card stack — The footer supports 1–3 stacked notification cards above Log Out. These are opt-in per product — omit them if the product does not need them.
What the master does NOT prescribe
- A fixed number of menu items — add as many or as few as the product requires.
- Mandatory notification cards — include them only when the product needs promotional or feature-awareness cards.
- Specific icons or labels — each product chooses its own Lucide icons and nav labels.
Master scaffold
<SidebarProvider>
<Sidebar collapsible="icon">
<SidebarHeader>
<Logo /> {/* Brand wordmark (expanded) / icon mark (collapsed) */}
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarMenu>
{/* Repeat SidebarMenuItem for each nav item the product needs */}
<SidebarMenuItem>
<SidebarMenuButton isActive={…} tooltip="Label">
<Icon className="size-4" />
<span>Label</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
{/* Optional: notification card stack */}
<SidebarMenuButton tooltip="Log Out">
<LogOut className="size-4" />
<span>Log Out</span>
</SidebarMenuButton>
</SidebarFooter>
</Sidebar>
</SidebarProvider>
Collapsed State (Icon Rail)
The sidebar supports a collapsible="icon" mode that collapses to a 48px icon-only rail.
| Part | Collapsed behavior |
|---|---|
| Sidebar | w-12 (48px), content centered |
| SidebarHeader | Logo collapses to icon mark only (brand icon in rounded-lg container) |
| SidebarMenuButton | Fixed size-10 (40×40px) square, rounded-md, icon centered, no text |
| SidebarMenuButton tooltip | Appears to the right of the icon on hover via the tooltip prop |
| SidebarMenuBadge | Remains visible, positioned top-right of the icon button |
| Sub-menus | Hidden — only top-level icons shown. Clicking a collapsible item auto-expands the sidebar and opens its accordion. |
| Notification cards | Hidden in collapsed state |
| Log Out | Icon-only, same size-10 rounded-md treatment as nav items |
Collapse Trigger
Collapse is controlled externally via a SidebarTrigger (PanelLeft icon) in the page header — not inside the sidebar itself. The header component provides the toggle; the sidebar only responds to state.
// In the page Header component:
<SidebarTrigger>
<Button variant="ghost" size="icon" className="size-7">
<PanelLeft className="size-4" />
</Button>
</SidebarTrigger>
SidebarHeader (Logo)
The header displays the TimelyCare brand wordmark in expanded state and collapses to an icon mark placeholder.
import { Logo } from "@timelycare/helix-ui"
| State | Implementation |
|---|---|
| Expanded | <Logo variant="default" width={120} height={44} /> at px-6, vertically centered in h-16 |
| Collapsed | Brand icon mark (size-9, rounded-[10px], bg-primary) centered in rail |
The default variant (TC-logo_2C.svg) is used across all product platforms — member, campus, provider, and admin all share the same TimelyCare wordmark.
Logo click resets navigation
Clicking the logo (in either expanded or collapsed state) navigates the user back to the home page and resets the Header breadcrumbs to "Home". This provides a consistent "go home" affordance regardless of sidebar state.
<SidebarHeader>
<button onClick={() => navigate("/")} className="h-16 flex items-center px-6">
{/* Expanded: TimelyCare wordmark */}
<Logo variant="default" width={120} height={44} />
{/* Collapsed: icon mark placeholder */}
</button>
</SidebarHeader>
Menu Button
| Part | Tailwind |
|---|---|
| Container | h-10 gap-2 px-2 rounded-md (40px height matches collapsed size-10) |
| Icon | size-4 text-sidebar-foreground |
| Text | text-sm font-medium leading-normal text-sidebar-foreground |
| Active icon + text | font-semibold text-sidebar-accent-foreground |
| Hover | bg-sidebar-accent-hover text-sidebar-accent-hover-foreground |
| Collapsed button | size-10 rounded-md (40×40px square, icon centered) |
| Collapse animation | transition-all duration-200 on sidebar width |
Sub-Menu
| Part | Tailwind |
|---|---|
| SidebarMenuSub | pl-9 pr-2 py-0.5 border-l border-border ml-5 space-y-0.5 |
| SidebarMenuSubItem | px-2 py-1.5 rounded-md |
| Sub-item text | text-sm font-normal leading-none text-sidebar-foreground |
| Collapse trigger | ChevronDown size-4 on parent menu item, rotates 180° when open |
Accordion behavior
Only one collapsible sub-menu group can be expanded at a time. When the user opens a new group, the previously open group closes automatically. All sub-menus start closed by default — even if a child item is the active route, the sub-menu must be explicitly opened by the user or programmatically opened on navigation.
Selection promotion on accordion close
When the user closes an accordion that contains the currently selected sub-menu item:
- The parent menu item inherits the selected state (shows as active).
- The child selection is saved internally.
- If the user re-opens that same accordion without selecting anything else in between, the saved child item is restored as the active selection and the parent returns to default state.
- If the user selects a different item before re-opening, the saved child is discarded.
Collapsed state + collapsible items
When the sidebar is in collapsed (icon rail) mode and the user clicks a menu item that has sub-menu children:
- The sidebar automatically expands back to the default/open state.
- The clicked item's accordion opens to reveal its sub-menu items.
- The user can then select the specific sub-menu item they want.
This ensures sub-menu navigation is always accessible since sub-menus are not shown in the collapsed state.
const [openGroup, setOpenGroup] = useState<string | null>(null)
const [savedChildId, setSavedChildId] = useState<string | null>(null)
const handleToggleGroup = (group: string) => {
if (openGroup === group) {
// Closing — promote parent if a child is selected
if (selectedId?.startsWith(group + "/")) {
setSavedChildId(selectedId)
setSelectedId(group)
}
setOpenGroup(null)
} else {
// Opening — restore saved child if it belongs to this group
if (savedChildId?.startsWith(group + "/")) {
setSelectedId(savedChildId)
setSavedChildId(null)
}
setOpenGroup(group)
}
}
const handleSelect = (id: string) => {
setSelectedId(id)
setSavedChildId(null) // Clear saved child on any new selection
}
const handleExpandGroup = (group: string) => {
setIsCollapsed(false)
setOpenGroup(group)
}
Badge (Notification Count)
| Part | Tailwind |
|---|---|
| Container | min-w-5 h-5 px-1 rounded-full bg-primary |
| Text | text-xs font-bold text-primary-foreground |
| Collapsed position | absolute -top-0.5 -right-0.5 on the icon button |
<SidebarMenuButton>
<Mail className="size-4" />
<span>Messages</span>
<SidebarMenuBadge>8</SidebarMenuBadge>
</SidebarMenuButton>
States
| State | Implementation |
|---|---|
| Default | text-sidebar-foreground font-medium — no item is selected by default on page load |
| Hover | bg-sidebar-accent-hover text-sidebar-accent-hover-foreground |
| Active/Selected | bg-sidebar-accent text-sidebar-accent-foreground font-semibold |
| Focused | ring-2 ring-sidebar-ring ring-offset-2 |
Selection behavior: Nothing is selected by default. Selection is driven by user interaction (click/navigation). The
isActiveprop onSidebarMenuButtoncontrols the active visual state.
Parent vs child selection visibility
When a sub-menu child is selected:
- Accordion open: Only the child shows active. The parent menu item returns to default state.
- Accordion closed: The parent inherits the active state (child is not visible).
- Collapsed sidebar: The parent icon shows active (children are never visible in collapsed mode).
Sub-menu item states
Sub-menu items follow the same hover and selected styles as top-level menu items (hover uses sidebar-accent-hover + sidebar-accent-hover-foreground, selected uses sidebar-accent + sidebar-accent-foreground):
| State | Implementation |
|---|---|
| Default | text-sidebar-foreground |
| Hover | bg-sidebar-accent-hover text-sidebar-accent-hover-foreground |
| Active/Selected | bg-sidebar-accent text-sidebar-accent-foreground font-semibold |
Notification Card Stack (Footer)
An optional stack of overlapping notification cards in the SidebarFooter, positioned above the Log Out button.
| Property | Value |
|---|---|
| Position | Above Log Out in SidebarFooter |
| Stack direction | Additional cards peek from the top of the pile |
| Card styling | Follows Card component spec: rounded-xl shadow-sm border |
| Card title | text-base font-bold leading-none text-card-foreground |
| Card description | text-sm leading-5 text-muted-foreground |
| Card image | Optional, uses AspectRatio (5:4) |
| Card actions | View / Dismiss link buttons (text-xs font-semibold text-primary) |
| Visibility | Shown in expanded state only; hidden when sidebar is collapsed |
| Props | showCard2, showCard3 booleans control stack depth (1–3 cards) |
Footer alignment: The
SidebarFooterhasp-2. The nav content area has an additional innerp-2wrapper. To keep the Log Out button icon-aligned with nav items, wrap it in apx-2container (matching the nav's inner padding). The notification card stack also uses apx-2wrapper.
<SidebarFooter className="p-2">
{/* Optional notification card stack — px-2 aligns with nav items */}
<div className="px-2">
<SidebarCard
headerText="New Feature!"
description="Check out our new Breathwork Tool in Self-Care"
showImage
/>
</div>
{/* Log Out — px-2 wrapper aligns icon with nav items above */}
<div className="px-2">
<SidebarMenuButton tooltip="Log Out">
<LogOut className="size-4" />
<span>Log Out</span>
</SidebarMenuButton>
</div>
</SidebarFooter>
Common Patterns
Basic Navigation
<SidebarProvider>
<Sidebar collapsible="icon">
<SidebarHeader>
<Logo />
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton isActive tooltip="Dashboard">
<Home className="size-4" />
<span>Dashboard</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenuButton tooltip="Log Out">
<LogOut className="size-4" />
<span>Log Out</span>
</SidebarMenuButton>
</SidebarFooter>
</Sidebar>
</SidebarProvider>
Collapsible with Sub-menu (Accordion)
// openGroup state is managed at the sidebar level (see Accordion behavior above)
<Collapsible
open={openGroup === "visits"}
onOpenChange={() => handleToggleGroup("visits")}
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton>
<Calendar className="size-4" />
<span>Visits</span>
<ChevronDown className="ml-auto size-4 transition-transform group-data-[state=open]:rotate-180" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem>
<SidebarMenuSubButton isActive={selectedId === "visits/in-progress"}>
In Progress
</SidebarMenuSubButton>
</SidebarMenuSubItem>
<SidebarMenuSubItem>
<SidebarMenuSubButton>Scheduled</SidebarMenuSubButton>
</SidebarMenuSubItem>
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
With Badge and Notification Cards
<SidebarProvider>
<Sidebar collapsible="icon">
<SidebarHeader><Logo /></SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton tooltip="Messages">
<Mail className="size-4" />
<span>Messages</span>
<SidebarMenuBadge>8</SidebarMenuBadge>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarCard headerText="New Feature!" description="Try our Breathwork Tool" showImage />
<SidebarMenuButton tooltip="Log Out">
<LogOut className="size-4" />
<span>Log Out</span>
</SidebarMenuButton>
</SidebarFooter>
</Sidebar>
</SidebarProvider>
Product Variants (Reference Only)
The following are concrete implementations of the master sidebar for existing TimelyCare products. They are included as reference — use the master scaffold above as your starting point, not these product-specific configurations.
Four TimelyCare products use the sidebar with different navigation structures. All share the same component and styling — only the menu items, icons, and footer content differ.
| Product | Brand | Has sub-menus | Has notification cards |
|---|---|---|---|
| Member | timelyCare | No | Yes |
| Admin | timelyAdmin | Yes (Visits, Customers, Providers, Care) | No |
| Campus | timelyCampus | No | No |
| Provider | timelyProvider | No | No |
Member (timelyCare)
| Nav item | Lucide icon |
|---|---|
| Self-Care | HandHeart |
| Provider Care | ShieldUser |
| Community | MessageSquareText |
| Success Coaching | Trophy |
| Messages | Mail |
Admin (timelyAdmin)
| Nav item | Lucide icon | Sub-menu items |
|---|---|---|
| Dashboard | Grid2x2 | — |
| Queue | Clock | — |
| Visits | CalendarDays | In Progress, In Review, Scheduled, Past Visits, Cancelled |
| Members | CircleUser | — |
| Messages | Mail (badge: 8) | — |
| Customers | Landmark | Organizations, Customers, Campus Dashboard |
| Providers | ShieldUser | All Providers, Payments, Coverage |
| Users | Users | — |
| Community | MessageSquareText | — |
| Crisis Now | TriangleAlert | — |
| Care | HeartHandshake | Discharge Instructions |
Campus (timelyCampus)
| Nav item | Lucide icon |
|---|---|
| Members | CircleUser |
| Visits | CalendarDays |
| Referrals | ClipboardPaste |
| Messages | Mail |
| Client Card | IdCard |
| Resource Cards | LayoutList |
| Reports | ChartPie |
| Manage Accounts | Users |
| Account | UserCog |
Provider (timelyProvider)
| Nav item | Lucide icon |
|---|---|
| Schedule | CalendarDays |
| Queue | Clock |
| Members | CircleUser |
| Messages | Mail |
| Past Visits | ClipboardList |
| Reports | ChartPie |
| Policies | FileText |
Accessibility
- Custom component using Radix Sheet for mobile overlay
- Use
<nav>landmark element witharia-label="Main navigation" - Keyboard: Tab through nav items, Escape to close mobile sidebar
- Mobile sidebar: focus trapped via Sheet component
- Collapsible sections use
aria-expanded - Collapsed tooltips provide text labels for screen readers
Gotchas
| Problem | Solution |
|---|---|
| Icons wrong size | Use size-4 (16px) for menu, size-4 for collapse chevron |
| Icons look too thick or thin | Keep the default Lucide strokeWidth (2) — do not add a strokeWidth override. The default is correct at 16px and matches every other size-4 icon in the system |
| Sub-menu not indented | SidebarMenuSub has pl-9 ml-5 with left border |
| Badge not aligned | Place inside SidebarMenuButton, after text span |
| Collapsed mode icons | Icons remain size-4, centered in 40×40px square button |
| Icons drift vertically between expanded/collapsed | Expanded buttons must use h-10 (40px) to match the collapsed size-10 — do not use p-2 for vertical padding or the rows will be shorter and icons will appear to shift during the transition |
| Log Out hover uses wrong text color | Hover must use both bg-sidebar-accent-hover and text-sidebar-accent-hover-foreground — do not hard-code a different foreground |
| Collapsed tooltip not visible | Use tooltip prop on SidebarMenuButton — renders via Radix portaled tooltip (not manual fixed positioning) |
| Notification cards visible when collapsed | Cards are hidden in collapsed state — only show expanded |
| Toggle button inside sidebar | Collapse is controlled externally via SidebarTrigger in the page Header |
| Group headings showing | TimelyCare sidebars use flat nav without SidebarGroupLabel |
| Multiple sub-menus open at once | Use accordion pattern — only one collapsible group open at a time |
| Sub-menu open by default | All sub-menus start closed; open state is user-driven or set programmatically |
| Sub-menu items look different from main items | Sub-menu items use the same hover (sidebar-accent-hover + sidebar-accent-hover-foreground) and selected (sidebar-accent + sidebar-accent-foreground) styles as top-level menu items |
| Item pre-selected on load | No item should be selected by default; selection reflects actual navigation state |
| Parent loses selection when accordion opens | By design — when accordion opens and a saved child exists, selection transfers back to the child |
| Collapsible item unclickable when collapsed | Clicking a collapsible item in collapsed mode auto-expands the sidebar and opens the accordion |
| Log Out icon misaligned with nav items | Wrap expanded Log Out button in px-2 div to match nav's inner p-2 padding |
| Parent shows selected while accordion is open | Parent should only show selected when accordion is closed; when open, only the child shows selected |
| Logo not clickable | Wrap the logo in a <button> or <a> that navigates home and resets header breadcrumbs |
| Header border misaligned with sidebar header | Both must use h-16 with border-b on the same element — no extra wrapper |
| Page footer bar border misaligned with sidebar footer | The page-level footer bar is the height reference: it uses h-16 (64px) to match the header's height. Override SidebarFooter with className="py-4" (16px + 32px button + 16px = 64px) to align its border-t with the page footer's border-t. Never shrink the page footer to match the sidebar's natural height. |
See Also
- Related Components: Navigation Menu (top navigation), Header (app header with SidebarTrigger and breadcrumbs), Breadcrumb (breadcrumb primitives), Card (notification card styling)
- Accessibility: Navigation Accessibility — Sidebar landmark patterns
Last updated: February 27, 2026