Light

Sidebar

A composable, themeable and customizable sidebar component.

Spec · from metadata

When to use

  • Application-level navigation sidebar
  • Dashboard layouts with collapsible navigation
  • Multi-section sidebar with headers, content, footers, and groups

When not to use

  • Simple navigation menu — use NavigationMenu instead
  • Slide-out panels without nav — use Sheet instead
  • Settings drawer — use Sheet instead

Variants

PropValuesDefaultDescription
sideleftrightleftWhich side the sidebar appears on.
variantsidebarfloatinginsetsidebarsidebar has border; floating has shadow and rounded corners; inset pads the content area.
collapsibleoffcanvasiconnoneoffcanvasoffcanvas slides completely off-screen; icon collapses to icon-only width; none is always visible.
variant (SidebarMenuButton)defaultoutlinedefaultdefault has hover bg; outline adds a border shadow.
size (SidebarMenuButton)defaultsmlgdefaultControls height: sm=28px, default=32px, lg=48px.

Anti-patterns

Avoid<Sidebar>
  <SidebarContent>...</SidebarContent>
</Sidebar>
Prefer<SidebarProvider>
  <Sidebar>
    <SidebarContent>...</SidebarContent>
  </Sidebar>
  <SidebarInset>...</SidebarInset>
</SidebarProvider>

Sidebar must be wrapped in SidebarProvider for state management and context.

Avoid<SidebarMenuButton>
  <a href="/page">Page</a>
</SidebarMenuButton>
Prefer<SidebarMenuButton asChild>
  <a href="/page">Page</a>
</SidebarMenuButton>

Use asChild to render as the child element (anchor) instead of wrapping in a button.

Accessibility

  • Screen readerSidebarTrigger includes sr-only 'Toggle Sidebar' text. Mobile mode uses Sheet with sr-only title and description. SidebarMenuButton tooltip is hidden when expanded or on mobile.
  • ContrastUses sidebar-specific tokens (sidebar, sidebar-foreground, sidebar-accent, etc.) for independent theming.

Token bindings

TokenCategoryUsage
sidebarcolorSidebar background
sidebar-foregroundcolorSidebar text
sidebar-accentcolorActive menu item background
sidebar-accent-foregroundcolorActive menu item text
sidebar-bordercolorBorder and separator color
sidebar-ringcolorFocus ring color
--sidebar-widthspacingDesktop sidebar width (16rem)
--sidebar-width-iconspacingCollapsed icon mode width (3rem)

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

PartTailwind
Sidebar (expanded)w-[248px] bg-sidebar border-r border-sidebar-border
Sidebar (collapsed)w-12 (48px, icon-only rail)
SidebarHeaderh-16 px-6 py-3 border-b border-sidebar-border
SidebarContentflex-1 overflow-y-auto p-2
SidebarFooterp-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).
SidebarGroupp-2
SidebarMenuflex flex-col

Note: The TimelyCare sidebar uses a flat navigation without SidebarGroupLabel headings. All menu items sit in a single SidebarGroup with no subheadings. SidebarGroupLabel is 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

  1. Menu item sizing and styles — Every menu item uses size-4 (16px) icons, text-sm font-medium, h-10 gap-2 px-2 rounded-md spacing (40px fixed height), and the active/hover state tokens listed in the States section. In collapsed mode, each item renders as a size-10 (40×40px) square button with the icon centered. The expanded h-10 and collapsed size-10 ensure icons stay at the same vertical position during the expand/collapse transition.
  2. Log Out button in the footer — Every Helix sidebar places a visible LogOut icon + text button in SidebarFooter. Do not use the default shadcn avatar/dropdown menu pattern.
  3. 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.

PartCollapsed behavior
Sidebarw-12 (48px), content centered
SidebarHeaderLogo collapses to icon mark only (brand icon in rounded-lg container)
SidebarMenuButtonFixed size-10 (40×40px) square, rounded-md, icon centered, no text
SidebarMenuButton tooltipAppears to the right of the icon on hover via the tooltip prop
SidebarMenuBadgeRemains visible, positioned top-right of the icon button
Sub-menusHidden — only top-level icons shown. Clicking a collapsible item auto-expands the sidebar and opens its accordion.
Notification cardsHidden in collapsed state
Log OutIcon-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"
StateImplementation
Expanded<Logo variant="default" width={120} height={44} /> at px-6, vertically centered in h-16
CollapsedBrand 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

PartTailwind
Containerh-10 gap-2 px-2 rounded-md (40px height matches collapsed size-10)
Iconsize-4 text-sidebar-foreground
Texttext-sm font-medium leading-normal text-sidebar-foreground
Active icon + textfont-semibold text-sidebar-accent-foreground
Hoverbg-sidebar-accent-hover text-sidebar-accent-hover-foreground
Collapsed buttonsize-10 rounded-md (40×40px square, icon centered)
Collapse animationtransition-all duration-200 on sidebar width

Sub-Menu

PartTailwind
SidebarMenuSubpl-9 pr-2 py-0.5 border-l border-border ml-5 space-y-0.5
SidebarMenuSubItempx-2 py-1.5 rounded-md
Sub-item texttext-sm font-normal leading-none text-sidebar-foreground
Collapse triggerChevronDown 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:

  1. The parent menu item inherits the selected state (shows as active).
  2. The child selection is saved internally.
  3. 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.
  4. 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:

  1. The sidebar automatically expands back to the default/open state.
  2. The clicked item's accordion opens to reveal its sub-menu items.
  3. 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)

PartTailwind
Containermin-w-5 h-5 px-1 rounded-full bg-primary
Texttext-xs font-bold text-primary-foreground
Collapsed positionabsolute -top-0.5 -right-0.5 on the icon button
<SidebarMenuButton>
  <Mail className="size-4" />
  <span>Messages</span>
  <SidebarMenuBadge>8</SidebarMenuBadge>
</SidebarMenuButton>

States

StateImplementation
Defaulttext-sidebar-foreground font-medium — no item is selected by default on page load
Hoverbg-sidebar-accent-hover text-sidebar-accent-hover-foreground
Active/Selectedbg-sidebar-accent text-sidebar-accent-foreground font-semibold
Focusedring-2 ring-sidebar-ring ring-offset-2

Selection behavior: Nothing is selected by default. Selection is driven by user interaction (click/navigation). The isActive prop on SidebarMenuButton controls 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):

StateImplementation
Defaulttext-sidebar-foreground
Hoverbg-sidebar-accent-hover text-sidebar-accent-hover-foreground
Active/Selectedbg-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.

PropertyValue
PositionAbove Log Out in SidebarFooter
Stack directionAdditional cards peek from the top of the pile
Card stylingFollows Card component spec: rounded-xl shadow-sm border
Card titletext-base font-bold leading-none text-card-foreground
Card descriptiontext-sm leading-5 text-muted-foreground
Card imageOptional, uses AspectRatio (5:4)
Card actionsView / Dismiss link buttons (text-xs font-semibold text-primary)
VisibilityShown in expanded state only; hidden when sidebar is collapsed
PropsshowCard2, showCard3 booleans control stack depth (1–3 cards)

Footer alignment: The SidebarFooter has p-2. The nav content area has an additional inner p-2 wrapper. To keep the Log Out button icon-aligned with nav items, wrap it in a px-2 container (matching the nav's inner padding). The notification card stack also uses a px-2 wrapper.

<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.

ProductBrandHas sub-menusHas notification cards
MembertimelyCareNoYes
AdmintimelyAdminYes (Visits, Customers, Providers, Care)No
CampustimelyCampusNoNo
ProvidertimelyProviderNoNo

Member (timelyCare)

Nav itemLucide icon
Self-CareHandHeart
Provider CareShieldUser
CommunityMessageSquareText
Success CoachingTrophy
MessagesMail

Admin (timelyAdmin)

Nav itemLucide iconSub-menu items
DashboardGrid2x2
QueueClock
VisitsCalendarDaysIn Progress, In Review, Scheduled, Past Visits, Cancelled
MembersCircleUser
MessagesMail (badge: 8)
CustomersLandmarkOrganizations, Customers, Campus Dashboard
ProvidersShieldUserAll Providers, Payments, Coverage
UsersUsers
CommunityMessageSquareText
Crisis NowTriangleAlert
CareHeartHandshakeDischarge Instructions

Campus (timelyCampus)

Nav itemLucide icon
MembersCircleUser
VisitsCalendarDays
ReferralsClipboardPaste
MessagesMail
Client CardIdCard
Resource CardsLayoutList
ReportsChartPie
Manage AccountsUsers
AccountUserCog

Provider (timelyProvider)

Nav itemLucide icon
ScheduleCalendarDays
QueueClock
MembersCircleUser
MessagesMail
Past VisitsClipboardList
ReportsChartPie
PoliciesFileText

Accessibility

  • Custom component using Radix Sheet for mobile overlay
  • Use <nav> landmark element with aria-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

ProblemSolution
Icons wrong sizeUse size-4 (16px) for menu, size-4 for collapse chevron
Icons look too thick or thinKeep 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 indentedSidebarMenuSub has pl-9 ml-5 with left border
Badge not alignedPlace inside SidebarMenuButton, after text span
Collapsed mode iconsIcons remain size-4, centered in 40×40px square button
Icons drift vertically between expanded/collapsedExpanded 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 colorHover must use both bg-sidebar-accent-hover and text-sidebar-accent-hover-foreground — do not hard-code a different foreground
Collapsed tooltip not visibleUse tooltip prop on SidebarMenuButton — renders via Radix portaled tooltip (not manual fixed positioning)
Notification cards visible when collapsedCards are hidden in collapsed state — only show expanded
Toggle button inside sidebarCollapse is controlled externally via SidebarTrigger in the page Header
Group headings showingTimelyCare sidebars use flat nav without SidebarGroupLabel
Multiple sub-menus open at onceUse accordion pattern — only one collapsible group open at a time
Sub-menu open by defaultAll sub-menus start closed; open state is user-driven or set programmatically
Sub-menu items look different from main itemsSub-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 loadNo item should be selected by default; selection reflects actual navigation state
Parent loses selection when accordion opensBy design — when accordion opens and a saved child exists, selection transfers back to the child
Collapsible item unclickable when collapsedClicking a collapsible item in collapsed mode auto-expands the sidebar and opens the accordion
Log Out icon misaligned with nav itemsWrap expanded Log Out button in px-2 div to match nav's inner p-2 padding
Parent shows selected while accordion is openParent should only show selected when accordion is closed; when open, only the child shows selected
Logo not clickableWrap the logo in a <button> or <a> that navigates home and resets header breadcrumbs
Header border misaligned with sidebar headerBoth must use h-16 with border-b on the same element — no extra wrapper
Page footer bar border misaligned with sidebar footerThe 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


Last updated: February 27, 2026