apx-dsv0.1Local renderer · live
K
Foundations4
  • Getting started
  • Theming
  • Templates
  • Icons
  • Avatar
  • Badge
  • DataGrid
  • Scheduler
  • Stat
  • Table
  • Timeline
  • TreeView
  • Accordion
  • Alert
  • EmptyState
  • Progress
  • Skeleton
  • Spinner
  • SplashScreen
  • Toast
  • ColorPicker
  • FileUpload
  • Form
  • Rating
  • TagsInput
  • Combobox
  • Field
  • Select
  • Toggle
  • Button
  • Calendar
  • Checkbox
  • DatePicker
  • Input
  • NumberInput
  • Radio
  • Slider
  • Switch
  • Textarea
  • AppShell
  • Div
  • Divider
  • Sidebar
  • Stack
  • Typography
  • Image
  • Breadcrumbs
  • Carousel
  • NavigationMenu
  • Pagination
  • Stepper
  • Tabs
  • Toolbar
  • CommandPalette
  • Confirm
  • Drawer
  • HoverCard
  • Menu
  • Modal
  • Popover
  • Tooltip
  • Icon
  • Card
  • PricingCard
60 componentsapx-ds/renderer
Sidebar
Variant↳ other

Layout

Sidebar

Vertical navigation rail with sections, items, badges, expandable groups, and rail mode.

Sidebar

<Sidebar /> is the vertical navigation rail that lives inside an AppShell's sidebar slot — or stands alone with its own width — and powers the left-hand nav of dashboards, admin panels, docs sites, and internal tools.

Overview — sections, badges, and an expanded item

Loading preview…
Overview.tsx
  • Compound primitive: Sidebar.Header, Sidebar.Section, Sidebar.Item, Sidebar.SubItems, Sidebar.Spacer, Sidebar.Footer.
  • Rail mode (collapsed): item labels go sr-only, items auto-wrap in Tooltips, section labels collapse. Wired to useAppShell().isSidebarCollapsed when inside an AppShell.
  • Active-state matching at the root via activeHref + activeMatchStrategy (exact or prefix). Each Item self-decides aria-current="page" against the pure helper isActiveHref({ current, itemHref, strategy }).
  • Compound interactive elements — <a> for nav targets, <button> for actions, <button aria-expanded> for expandable groups. No roving tabindex (each item is a tab stop, matching the W3C Disclosure pattern).
  • asChild anywhere (<Sidebar.Item asChild>) for router-Link integration.

Anatomy

tsx
import { Sidebar, useAppShell } from 'apx-ds';

function AppNav() {
  const { isSidebarCollapsed } = useAppShell();
  return (
    <Sidebar collapsed={isSidebarCollapsed} activeHref={router.pathname}>
      <Sidebar.Header>
        <Logo />
      </Sidebar.Header>
      <Sidebar.Section label="Workspace">
        <Sidebar.Item href="/" icon={<HomeIcon />}>Home</Sidebar.Item>
        <Sidebar.Item href="/inbox" icon={<InboxIcon />} badge={3}>Inbox</Sidebar.Item>
      </Sidebar.Section>
      <Sidebar.Section label="Projects" collapsible defaultOpen>
        <Sidebar.Item href="/p/launch" icon={<FolderIcon />}>Launch</Sidebar.Item>
      </Sidebar.Section>
      <Sidebar.Spacer />
      <Sidebar.Footer>
        <UserMenu />
      </Sidebar.Footer>
    </Sidebar>
  );
}
import { Sidebar, useAppShell } from 'apx-ds';

function AppNav() {
  const { isSidebarCollapsed } = useAppShell();
  return (
    <Sidebar collapsed={isSidebarCollapsed} activeHref={router.pathname}>
      <Sidebar.Header>
        <Logo />
      </Sidebar.Header>
      <Sidebar.Section label="Workspace">
        <Sidebar.Item href="/" icon={<HomeIcon />}>Home</Sidebar.Item>
        <Sidebar.Item href="/inbox" icon={<InboxIcon />} badge={3}>Inbox</Sidebar.Item>
      </Sidebar.Section>
      <Sidebar.Section label="Projects" collapsible defaultOpen>
        <Sidebar.Item href="/p/launch" icon={<FolderIcon />}>Launch</Sidebar.Item>
      </Sidebar.Section>
      <Sidebar.Spacer />
      <Sidebar.Footer>
        <UserMenu />
      </Sidebar.Footer>
    </Sidebar>
  );
}

API — <Sidebar> root

PropTypeDefaultNotes
variantdefault · bordered · floating · ghostdefaultChrome family.
sizesm · md · lgmdPropagates to subparts via context.
itemSizesm · md · lginherits sizeOverride the per-item size axis only.
collapsedbooleanfalseRail mode — labels go sr-only, Tooltips kick in.
activeHrefstring—Current URL path.
activeMatchStrategyexact · prefixexactHow items decide they're active.
ariaLabelstring"Sidebar"Accessible name for the <nav> landmark.
ariaLabelledBystring—Alternative to ariaLabel.
widthnumber · string—Inline width (omit when inside AppShell).
collapsedWidthnumber · string—Inline width when collapsed.
positionstart · endstartLogical side; affects only variant="bordered".

API — <Sidebar.Item>

PropTypeDefaultNotes
iconReactNode—Leading icon. Required in rail mode (only visible content).
endIconReactNode—Trailing icon. Hidden in rail mode.
badgeReactNode · number—Renders inside a <Badge>. Hidden visibly in rail; mirrored sr-only for AT.
badgeColorBadgeColorneutralPalette for the badge.
hrefstring—When set, item renders as <a href>.
activeboolean—Explicit override. Otherwise computed from activeHref + strategy.
expandablebooleanfalseItem becomes a disclosure trigger for nested <Sidebar.SubItems> children.
defaultExpandedbooleanfalseInitial uncontrolled expanded state.
expandedboolean—Controlled expanded state.
onExpandedChange(expanded: boolean) => void—Fires on every transition (controlled or uncontrolled).
variantdefault · ghost · primarydefaultPer-item visual variant.
sizesm · md · lginheritsPer-item size override.
disabledbooleanfalseSets aria-disabled + neutralizes clicks.
asChildbooleanfalseRender the consumer's single child element with merged props (router-Link pattern).

API — <Sidebar.Section>

PropTypeDefaultNotes
labelReactNode—Section heading. Required for screen readers.
collapsiblebooleanfalseWhen true, label becomes a <button aria-expanded>.
defaultOpenbooleantrueInitial uncontrolled open state.
openboolean—Controlled open state.
onOpenChange(open: boolean) => void—Fires on every transition.
hideLabelWhenCollapsedbooleantrueHide the section label visually when the sidebar is collapsed.
badgeReactNode · number—Optional badge beside the label.
badgeColorBadgeColorneutralBadge palette.

Active-href matching

The pure helper isActiveHref({ current, itemHref, strategy }) decides each item's active state. Trailing slashes are normalized. Under prefix, the boundary check ensures /p does not wrongly match /photos — the helper requires current to start with itemHref + '/'.

ts
import { isActiveHref } from 'apx-ds';

isActiveHref({ current: '/inbox/42', itemHref: '/inbox', strategy: 'prefix' }); // true
isActiveHref({ current: '/photos',  itemHref: '/p',     strategy: 'prefix' }); // false
isActiveHref({ current: '/inbox/',  itemHref: '/inbox', strategy: 'exact'  }); // true
import { isActiveHref } from 'apx-ds';

isActiveHref({ current: '/inbox/42', itemHref: '/inbox', strategy: 'prefix' }); // true
isActiveHref({ current: '/photos',  itemHref: '/p',     strategy: 'prefix' }); // false
isActiveHref({ current: '/inbox/',  itemHref: '/inbox', strategy: 'exact'  }); // true

Rail (collapsed) mode

Set collapsed on the root — typically wired to useAppShell().isSidebarCollapsed:

  • Item labels become sr-only (kept in the DOM for SR announcement).
  • Each item is wrapped in a <Tooltip content={label} placement="right"> so the visible cue on hover/focus is the label text itself.
  • Section labels go sr-only by default; opt out via hideLabelWhenCollapsed={false}.
  • End icons + visible badges hide; numeric badge contents are mirrored to a sr-only span so unread counts still reach AT.

Examples

Basic flat — minimal sidebar

Loading preview…
BasicFlat.tsx

With sections — labeled groupings

Loading preview…
WithSections.tsx

Collapsible sections — Accordion-style folding

Loading preview…
WithCollapsibleSections.tsx

Expandable items — two-level docs nav

Loading preview…
WithExpandableItem.tsx

Header + Footer — full chrome

Loading preview…
WithHeaderFooter.tsx

Badges — color spectrum showcase

Loading preview…
WithBadges.tsx

Spacer — push CTA to bottom

Loading preview…
WithSpacer.tsx

Rail mode — Tooltip-driven icon rail

Loading preview…
RailMode.tsx

Router Link via asChild

Loading preview…
RouterLinkIntegration.tsx

Active matching — prefix strategy with boundary

Loading preview…
ActiveHrefPrefix.tsx

All four chrome variants

Loading preview…
Variants.tsx

Three size scales

Loading preview…
Sizes.tsx

Disabled items — paid-tier gating

Loading preview…
Disabled.tsx

Dashboard — Sidebar inside AppShell with rail toggle

Loading preview…
DashboardDemo.tsx

Accessibility

  • Root renders as <nav aria-label={ariaLabel}> (default "Sidebar"). Does NOT use <aside> because AppShell already labels its own <aside> sidebar slot; nesting two landmarks would add noise.
  • <Sidebar.Item href> → <a> (navigation target). With aria-current="page" when active.
  • <Sidebar.Item onClick> → <button type="button">.
  • <Sidebar.Item expandable> → <button aria-expanded aria-controls> (Disclosure pattern).
  • <Sidebar.SubItems> → <ul role="group">.
  • Section labels: static → <h3>; collapsible → <button aria-expanded>.
  • Disabled items get aria-disabled="true" and tabIndex={-1}; clicks are neutralized.
  • No roving tabindex — every item is a tab stop. This matches Linear / GitHub / Notion / VS Code, and the W3C Disclosure / Landmarks patterns (navigation is browsable with Tab; role="menu" patterns are for menus, not navigation).
  • axe-core: zero violations across flat / sections / collapsible / expandable / rail / active configurations.

RTL

  • The root flex column is direction-agnostic.
  • Inline borders use border-inline-end / border-inline-start so the bordered variant lands on the correct logical side.
  • Padding + icon order use logical properties throughout.
  • Rail-mode Tooltip uses placement="right" which flips via Floating UI's flip middleware under dir="rtl".

Theming

Sidebar reads from theme.components.Sidebar.styleOverrides.{root, item, sectionLabel, sectionBody, disclosure, header, footer, subItems}. Consumer className always wins via tailwind-merge.

Do / Don't

  • Do wire collapsed to useAppShell().isSidebarCollapsed when nested in AppShell. That's the canonical pattern — Sidebar then degrades into a rail in sync with the header hamburger.
  • Do use activeMatchStrategy="prefix" for nested route highlighting; the boundary check is built-in so /p does not match /photos.
  • Do put icons on every item that might appear in rail mode — without an icon the rail has nothing visible to show.
  • Do use asChild to integrate with your router (<RouterLink>, <NavLink>, etc.) instead of adding onClick={navigate} handlers.
  • Don't add roving tabindex — sidebar items are tab stops, not menu items.
  • Don't nest a Sidebar inside another Sidebar. Use <Sidebar.Item expandable> + <Sidebar.SubItems> for hierarchical navigation.
  • Don't render a Sidebar as the AppShell's aside slot — the aside is for context panels (details / settings / chat), not navigation.

More examples

_icons

Loading preview…
_icons.tsx

Props

PropTypeDefaultDescription
activeHrefstring—The current page's URL path. Each `<Sidebar.Item href>` self-compares against this to decide if it's the active item; the active item then gets `aria-current="page"` + active styling.
activeMatchStrategyenum—How `activeHref` is matched against each Item's `href`. Default: `'exact'`. Use `'prefix'` to highlight a parent route when on a child page (`/projects` active when on `/projects/42`).
ariaLabelstring—Accessible name for the `<aside>` landmark. Required for axe-clean output unless the sidebar already lives inside an AppShell's labeled sidebar slot (AppShell labels its own `<aside>`, and the Sidebar component renders as a plain `<nav>` in that case via the `as` prop — but here Sidebar always renders `<nav>` to avoid double-landmark warnings). Default: `'Sidebar'`.
ariaLabelledBystring—Override the sidebar's own `<nav>` aria-labelledby instead of `ariaLabel`.
classNamestring——
collapsedboolean—When `true`, the sidebar enters **rail mode**: item labels become `sr-only` (the icon stays visible), each Item is wrapped in a Tooltip showing its label, and section labels collapse. Drives directly off the consumer's prop — typically wired to `useAppShell().isSidebarCollapsed` when nested inside an AppShell. Default: `false`.
collapsedWidthstring | number—Inline width override when `collapsed`. Defaults to the parent layout.
itemSizeenum—Per-Item size override (otherwise inherits `size`).
positionenum—Logical position relative to main content. Affects only the `bordered` variant's border side. Default: `'start'`.
sizeenum—Size scale propagated to subparts. Default: `'md'`.
styleCSSProperties——
sxSx——
variantenum—Visual chrome. Default: `'default'`.
widthstring | number—Optional inline width override (e.g. `'260px'` or `260`). Defaults to the parent layout.