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"
className?: string
}
interface SidebarMenuButtonProps {
asChild?: boolean
isActive?: boolean
variant?: "default" | "outline"
size?: "default" | "sm" | "lg"
}
Structure
| Part | Tailwind |
|---|---|
| Sidebar (expanded) | w-[248px] bg-sidebar border-r border-sidebar-border |
| SidebarHeader | base flex flex-col gap-2 p-2; the master scaffold applies h-16 px-6 border-b border-sidebar-border on the header (see usage example) |
| 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 top-level menu item uses
size-4(16px) icons,text-sm font-semibold,h-10 gap-2 px-2 rounded-mdspacing (40px fixed height), and the active/hover state tokens listed in the States section. Icons are passed bare (nosize/fill/className) — the button owns icon size ([&>svg]:size-4) and color (icons inheritcurrentColorfrom the button text). - 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>
<SidebarHeader>
<Logo />
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarMenu>
{/* Repeat SidebarMenuItem for each nav item the product needs */}
<SidebarMenuItem>
<SidebarMenuButton isActive={…}>
<Icon className="size-4" />
<span>Label</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
{/* Optional: notification card stack */}
<SidebarMenuButton>
<LogOut className="size-4" />
<span>Log Out</span>
</SidebarMenuButton>
</SidebarFooter>
</Sidebar>
</SidebarProvider>
SidebarHeader (Logo)
The header displays the TimelyCare brand wordmark.
import { Logo } from "@timelycare/helix-ui"
| State | Implementation |
|---|---|
| Logo | <Logo variant="default" width={120} height={44} /> at px-6, vertically centered in h-16 |
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 navigates the user back to the home page and resets the Header breadcrumbs to "Home". This provides a consistent "go home" affordance.
<SidebarHeader>
<button onClick={() => navigate("/")} className="h-16 flex items-center px-6">
<Logo variant="default" width={120} height={44} />
</button>
</SidebarHeader>
Menu Button
| Part | Tailwind |
|---|---|
| Container | h-10 gap-2 px-2 rounded-md (40px fixed height) |
| Icon | size-4 text-sidebar-foreground |
| Text | text-sm font-semibold leading-normal text-sidebar-foreground |
| Active icon + text | text-sidebar-accent-foreground — weight stays font-semibold; active changes color only, not weight |
| Hover | bg-sidebar-accent-hover text-sidebar-accent-hover-foreground |
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 |
| Expand 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.
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
}
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 |
<SidebarMenuButton>
<Mail className="size-4" />
<span>Messages</span>
<SidebarMenuBadge>8</SidebarMenuBadge>
</SidebarMenuButton>
States
| State | Implementation |
|---|---|
| Default | text-sidebar-foreground font-semibold — 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 — weight stays font-semibold (same as default); active is distinguished by color, not weight |
| 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).
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 (sub-items keep their default weight — lighter than the semibold top-level items, by design) |
| Hover | bg-sidebar-accent-hover text-sidebar-accent-hover-foreground |
| Active/Selected | bg-sidebar-accent text-sidebar-accent-foreground — active changes color only; sub-item weight is unchanged. Icons inherit currentColor, so they track the label color. |
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 above the Log Out button |
| 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>
<LogOut className="size-4" />
<span>Log Out</span>
</SidebarMenuButton>
</div>
</SidebarFooter>
Common Patterns
Basic Navigation
<SidebarProvider>
<Sidebar>
<SidebarHeader>
<Logo />
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton isActive>
<Home className="size-4" />
<span>Dashboard</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<SidebarMenuButton>
<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>
<SidebarHeader><Logo /></SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton>
<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>
<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
- Nav-group expanders use
aria-expanded
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 |
| 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 |
| 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 |
| 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: July 22, 2026