AppSidebar

Canonical left-rail navigation. Universal across all 6 TimelyCare products — no per-product variants. The structural shape (brand → nav items → optional footer with org context and help) is the same regardless of surface tier; differences in item count, depth, and density are expressed via props and shared tokens, not variants.

Spec · from metadata

When to use

  • Inside AppShell as the sidebar slot — every authenticated route
  • Flat nav for products with ≤8 items (TimelyPulse, K-12 School/Parent, Member)
  • Grouped nav for products with 8+ items (Admin) — opt in via type: 'group' items
  • Multi-tenant admin where users see scoped data — pass orgContext slot

When not to use

  • As a standalone component outside AppShell — it needs the SidebarProvider context
  • For in-page secondary nav — use SecondarySidebar (Tier 4 watch) or build inline
  • For mobile-only nav — Sidebar primitive already handles the responsive Sheet

Required companions

Environmental prerequisites this component needs beyond its props. Supply these or it won't work as expected — TypeScript won't catch a missing one.

CompanionKindWhy
AppShellproviderAppSidebar requires the SidebarProvider context that AppShell provides — never use it outside AppShell.

Anti-patterns

Avoid<AppSidebar variant="consumer" items={...} />
Prefer<AppSidebar items={...} />
// no variant prop — density and styling come from shared tokens

No variant prop exists by design. Per-product density was reviewed and rejected as a structural variant — it's just content length, not a shape difference. Adding a variant prop would re-introduce the drift the design system is meant to eliminate.

Avoid<AppSidebar
  items={[
    { type: "group", id: "main", label: "Main", children: [
      { type: "link", id: "home", label: "Home", href: "/" }
    ]}
  ]}
/>
// 3 nav items total — wrapped in a single group
Prefer<AppSidebar items={[
  { type: "link", id: "home", label: "Home", href: "/" },
  { type: "link", id: "members", label: "Members", href: "/members" },
  { type: "link", id: "reports", label: "Reports", href: "/reports" },
]} />

Groups are for products with 8+ items where collapsible nesting helps organize. Wrapping a small nav in groups adds noise without value.

Avoid<AppSidebar items={navItems} helpItem={<AppSidebarHelpItem onClick={openHelp} />} />
// consumer product (Member, K-12 Parent)
Prefer<AppSidebar items={navItems} />
// elsewhere, in the page chrome:
<HelpFAB onClick={openHelp} />

Consumer surfaces use a floating help button (FAB), not a sidebar help item. Mixing the two affordances breaks the surface-tier rule documented in helix-app-layouts.md.

Accessibility

  • Min touch target48px
  • Screen readerInherits Sidebar primitive a11y — sidebar is a complementary landmark; nav items use button/anchor semantics with proper labels. Group triggers expose aria-expanded.
  • ContrastActive item uses sidebar-accent + sidebar-accent-foreground; meets WCAG AA on the sidebar background.

Token bindings

TokenCategoryUsage
sidebarcolorSidebar surface
sidebar-foregroundcolorSidebar text
sidebar-accentcolorActive item background
sidebar-accent-foregroundcolorActive item text
sidebar-bordercolorSection separators
3 (12px) / 2 (8px)spacingItem padding / inter-item gap
Recent changes
  • new

    AppSidebar section-aware active routes + NavBreadcrumb

    2026-07-24

  • new

    Compound components join the catalog

    2026-07-09

[!NOTE] Compound component — an opinionated composition of Helix primitives. Composes: Sidebar, Collapsible.

Import

import { AppSidebar, AppSidebarOrgContext, AppSidebarHelpItem } from "@timelycare/helix-ui"

Requires the AppShell wrapper — see its page for the composition.

Usage

<AppSidebar
  brand={<Logo />}
  items={[
    { type: "link", id: "home", label: "Home", icon: <Home />, href: "/" },
    { type: "link", id: "members", label: "Members", icon: <Users />, href: "/members" },
    {
      type: "group",
      id: "visits",
      label: "Visits",
      icon: <Calendar />,
      children: [
        { type: "link", id: "queue", label: "Queue", href: "/visits/queue" },
        { type: "link", id: "scheduled", label: "Scheduled", href: "/visits/scheduled" },
      ],
    },
  ]}
  pathname={pathname}
  onItemClick={(id) => router.push(routes[id])}
/>

Active state & navigation

Pass pathname (the current route) and AppSidebar resolves the active item and its owning section itself — section-aware and most-specific, so nested and sibling detail routes (e.g. /clients/1074, a sibling of the /clients/list leaf) still highlight their section. Set match on an item when its route area differs from its href. Collapsible groups tidy on navigation: only the group owning the current route stays open after a route change, so the nav never accumulates a pile of open groups. activeItemId remains supported and overrides the derived leaf.