# Side nav

Package: @commenda/pathfinder@0.1.0
Page: /docs/components/side-nav
Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=714-2

```tsx
import { useState } from 'react';

import { Avatar } from '@commenda/pathfinder/avatar';
import { Dropdown } from '@commenda/pathfinder/dropdown';
import { BookOpen01Icon } from '@commenda/pathfinder/icons/BookOpen01Icon';
import { Building06Icon } from '@commenda/pathfinder/icons/Building06Icon';
import { CalendarIcon } from '@commenda/pathfinder/icons/CalendarIcon';
import { CheckDone01Icon } from '@commenda/pathfinder/icons/CheckDone01Icon';
import { CoinsStacked03Icon } from '@commenda/pathfinder/icons/CoinsStacked03Icon';
import { CoinsSwap01Icon } from '@commenda/pathfinder/icons/CoinsSwap01Icon';
import { CommendaBrandIcon } from '@commenda/pathfinder/icons/CommendaBrandIcon';
import { File06Icon } from '@commenda/pathfinder/icons/File06Icon';
import { FolderIcon } from '@commenda/pathfinder/icons/FolderIcon';
import { HelpCircleIcon } from '@commenda/pathfinder/icons/HelpCircleIcon';
import { HomeLineIcon } from '@commenda/pathfinder/icons/HomeLineIcon';
import { Inbox01Icon } from '@commenda/pathfinder/icons/Inbox01Icon';
import { PneumaIcon } from '@commenda/pathfinder/icons/PneumaIcon';
import { Settings01Icon } from '@commenda/pathfinder/icons/Settings01Icon';
import { ShoppingBag02Icon } from '@commenda/pathfinder/icons/ShoppingBag02Icon';
import { Users01Icon } from '@commenda/pathfinder/icons/Users01Icon';
import { CommendaLockupLogo } from '@commenda/pathfinder/logos/commenda/CommendaLockupLogo';
import { SideNav, type SideNavSection } from '@commenda/pathfinder/side-nav';

const workspaceSections: SideNavSection[] = [
  {
    id: 'main',
    items: [
      {
        kind: 'link',
        id: 'home',
        label: 'Home',
        icon: <HomeLineIcon />,
        linkConfig: { href: '#home' },
      },
      {
        kind: 'link',
        id: 'tasks',
        label: 'Tasks',
        icon: <CheckDone01Icon />,
        count: 4,
        linkConfig: { href: '#tasks' },
      },
      {
        kind: 'link',
        id: 'inbox',
        label: 'Inbox',
        icon: <Inbox01Icon />,
        count: 3,
        unread: true,
        linkConfig: { href: '#inbox' },
      },
    ],
  },
  {
    id: 'overview',
    label: 'Overview',
    collapsible: true,
    items: [
      {
        kind: 'link',
        id: 'entities',
        label: 'Legal entities',
        icon: <Building06Icon />,
        linkConfig: { href: '#entities' },
      },
      {
        kind: 'link',
        id: 'people',
        label: 'Key persons',
        icon: <Users01Icon />,
        linkConfig: { href: '#people' },
      },
      {
        kind: 'link',
        id: 'calendar',
        label: 'Compliance calendar',
        icon: <CalendarIcon />,
        linkConfig: { href: '#calendar' },
      },
      {
        kind: 'group',
        id: 'documents',
        label: 'Documents',
        icon: <FolderIcon />,
        linkConfig: { href: '#documents' },
        children: [
          {
            kind: 'link',
            id: 'formation',
            label: 'Formation',
            icon: <FolderIcon />,
            linkConfig: { href: '#formation' },
          },
          {
            kind: 'link',
            id: 'tax-docs',
            label: 'Tax',
            icon: <FolderIcon />,
            linkConfig: { href: '#tax-docs' },
          },
        ],
      },
    ],
  },
  {
    id: 'apps',
    label: 'My apps',
    collapsible: true,
    items: [
      {
        kind: 'link',
        id: 'indirect-tax',
        label: 'Global indirect tax',
        icon: <CoinsStacked03Icon />,
        linkConfig: { href: '#indirect-tax' },
      },
      {
        kind: 'link',
        id: 'reporting',
        label: 'Tax & reporting',
        icon: <File06Icon />,
        linkConfig: { href: '#reporting' },
      },
      {
        kind: 'link',
        id: 'accounting',
        label: 'Accounting',
        icon: <BookOpen01Icon />,
        linkConfig: { href: '#accounting' },
      },
      {
        kind: 'link',
        id: 'transfer',
        label: 'Transfer pricing',
        icon: <CoinsSwap01Icon />,
        linkConfig: { href: '#transfer' },
      },
      {
        kind: 'link',
        id: 'marketplace',
        label: 'Marketplace',
        icon: <ShoppingBag02Icon />,
        linkConfig: { href: '#marketplace' },
      },
    ],
  },
];

const chatSections: SideNavSection[] = [
  {
    id: 'chat-main',
    items: [
      {
        kind: 'link',
        id: 'new-chat',
        label: 'New chat',
        icon: <PneumaIcon />,
        linkConfig: { href: '#new-chat' },
      },
      {
        kind: 'link',
        id: 'agents',
        label: 'Agents',
        icon: <Users01Icon />,
        linkConfig: { href: '#agents' },
      },
    ],
  },
];

const accountFooter = (collapsed: boolean) => (
  <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
    {[
      {
        label: 'Settings',
        href: '#settings',
        icon: <Settings01Icon size={16} />,
      },
      {
        label: 'Help & support',
        href: '#help',
        icon: <HelpCircleIcon size={16} />,
      },
    ].map((item) => (
      <a
        key={item.href}
        href={item.href}
        aria-label={item.label}
        title={collapsed ? item.label : undefined}
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: 12,
          padding: collapsed ? 6 : '6px 8px',
          minHeight: 28,
          boxSizing: 'border-box',
          color: 'inherit',
          textDecoration: 'none',
          borderRadius: 6,
        }}
      >
        {item.icon}
        {!collapsed && item.label}
      </a>
    ))}
    <Dropdown
      menu={{
        groups: [
          {
            id: 'account',
            items: [
              {
                id: 'profile',
                type: 'link',
                label: 'Your profile',
                linkConfig: { href: '#profile' },
              },
              {
                id: 'organization',
                type: 'link',
                label: 'Organization settings',
                linkConfig: { href: '#organization' },
              },
            ],
          },
        ],
      }}
      trigger={
        <button
          type="button"
          aria-label="Account menu for Sam Schulte"
          style={{
            display: 'flex',
            alignItems: 'center',
            gap: 8,
            padding: 4,
            border: 0,
            background: 'transparent',
            font: 'inherit',
            color: 'inherit',
            textAlign: 'start',
            borderRadius: 6,
          }}
        >
          <Avatar
            identity={{ kind: 'person', userId: 'sam', name: 'Sam Schulte' }}
            size={20}
            decorative
          />
          {!collapsed && 'Sam Schulte'}
        </button>
      }
    />
  </div>
);

export const SideNavDefault = () => {
  const [mode, setMode] = useState('home');
  const [active, setActive] = useState<string | null>('home');

  return (
    <div style={{ height: 720, display: 'flex', gap: 16, minWidth: 0 }}>
      <SideNav
        label="Workspace"
        activeId={active}
        sections={mode === 'home' ? workspaceSections : chatSections}
        header={(collapsed) =>
          collapsed ? (
            <CommendaBrandIcon size={20} label="Commenda" />
          ) : (
            <CommendaLockupLogo label="Commenda" />
          )
        }
        footer={accountFooter}
        modeConfig={{
          label: 'Workspace mode',
          value: mode,
          onValueChange: setMode,
          options: [
            { value: 'home', label: 'Home', leadingIcon: <HomeLineIcon /> },
            { value: 'chat', label: 'Chat', leadingIcon: <PneumaIcon /> },
          ],
        }}
        renderLink={(props) => (
          <a
            {...props}
            href={props.href}
            onClick={(event) => {
              event.preventDefault();
              setActive(props.href?.slice(1) ?? null);
            }}
          >
            {props.children}
          </a>
        )}
      />
    </div>
  );
};
```


Import `SideNav` from `@commenda/pathfinder/side-nav`. The application owns routes, active destination, permissions and persistence. No workspace providers, cookies or global shortcuts are required.

## API reference [#api-reference]

| Prop | Type | Requirement | Behavior |
| --- | --- | --- | --- |
| `label` | `string` | required | Required navigation landmark name |
| `sections` | `readonly SideNavSection[]` | required | IDs, optional label/collapsible, recursive items |
| `activeId` | `string \| null` | required | Current destination; receives aria-current=page |
| `collapseConfig` | `SelectionProps<boolean>` | optional | `{ value, onValueChange }` or `{ defaultValue?, onValueChange? }`; false initially |
| `expansionConfig` | `SelectionProps<readonly string[]>` | optional | Section and group IDs; collapsible sections start open |
| `header` | `ReactNode \| ((collapsed: boolean) => ReactNode)` | optional | Slots can adapt to the rail |
| `footer` | `ReactNode \| ((collapsed: boolean) => ReactNode)` | optional | Slots can adapt to the rail |
| `modeConfig` | `SegmentedControlProps` | optional | Optional view-mode switch; app supplies matching sections |
| `renderLink` | `((props: React.ComponentPropsWithRef<"a">) => React.ReactElement)` | optional | Next Link integration; item renderer takes precedence |
| `className` | `string` | optional | Root nav classes; parent supplies available height |

### collapseConfig fields

Choose one shape; excluded fields cannot be combined.

#### Option 1

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `value` | `boolean` | Yes |  |
| `onValueChange` | `(value: boolean) => void` | Yes |  |

Do not supply: `defaultValue`.

#### Option 2

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `defaultValue` | `boolean` | No |  |
| `onValueChange` | `((value: boolean) => void)` | No |  |

Do not supply: `value`.

### expansionConfig fields

Choose one shape; excluded fields cannot be combined.

#### Option 1

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `value` | `readonly string[]` | Yes |  |
| `onValueChange` | `(value: readonly string[]) => void` | Yes |  |

Do not supply: `defaultValue`.

#### Option 2

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `defaultValue` | `readonly string[]` | No |  |
| `onValueChange` | `((value: readonly string[]) => void)` | No |  |

Do not supply: `value`.

An item has `id`, `label`, `icon`, optional `count`, `unread`, and `disableConfig: { isDisabled, reason? }`. Its `kind` distinguishes `link` with `linkConfig`, `action` with `onAction`, or `group` with recursive `children` and optional landing `linkConfig`. A landing link and its chevron are distinct targets. A group without a landing link expands on activation.

Keep section and item IDs stable and globally distinct. `linkConfig` supports `href`, `target`, `rel`, and a ref-forwarding `render`. Renderers must preserve supplied accessibility props, handlers and refs. Actions and links are mutually exclusive. Counts and unread state stay accessible in the rail.

## Collapsed rail [#collapsed-rail]


```tsx
import { Avatar } from '@commenda/pathfinder/avatar';
import { Dropdown } from '@commenda/pathfinder/dropdown';
import { BookOpen01Icon } from '@commenda/pathfinder/icons/BookOpen01Icon';
import { Building06Icon } from '@commenda/pathfinder/icons/Building06Icon';
import { CalendarIcon } from '@commenda/pathfinder/icons/CalendarIcon';
import { CheckDone01Icon } from '@commenda/pathfinder/icons/CheckDone01Icon';
import { CoinsStacked03Icon } from '@commenda/pathfinder/icons/CoinsStacked03Icon';
import { CoinsSwap01Icon } from '@commenda/pathfinder/icons/CoinsSwap01Icon';
import { CommendaBrandIcon } from '@commenda/pathfinder/icons/CommendaBrandIcon';
import { File06Icon } from '@commenda/pathfinder/icons/File06Icon';
import { FolderIcon } from '@commenda/pathfinder/icons/FolderIcon';
import { HelpCircleIcon } from '@commenda/pathfinder/icons/HelpCircleIcon';
import { HomeLineIcon } from '@commenda/pathfinder/icons/HomeLineIcon';
import { Inbox01Icon } from '@commenda/pathfinder/icons/Inbox01Icon';
import { Settings01Icon } from '@commenda/pathfinder/icons/Settings01Icon';
import { ShoppingBag02Icon } from '@commenda/pathfinder/icons/ShoppingBag02Icon';
import { Users01Icon } from '@commenda/pathfinder/icons/Users01Icon';
import { CommendaLockupLogo } from '@commenda/pathfinder/logos/commenda/CommendaLockupLogo';
import { SideNav, type SideNavSection } from '@commenda/pathfinder/side-nav';

const workspaceSections: SideNavSection[] = [
  {
    id: 'main',
    items: [
      {
        kind: 'link',
        id: 'home',
        label: 'Home',
        icon: <HomeLineIcon />,
        linkConfig: { href: '#home' },
      },
      {
        kind: 'link',
        id: 'tasks',
        label: 'Tasks',
        icon: <CheckDone01Icon />,
        count: 4,
        linkConfig: { href: '#tasks' },
      },
      {
        kind: 'link',
        id: 'inbox',
        label: 'Inbox',
        icon: <Inbox01Icon />,
        count: 3,
        unread: true,
        linkConfig: { href: '#inbox' },
      },
    ],
  },
  {
    id: 'overview',
    label: 'Overview',
    collapsible: true,
    items: [
      {
        kind: 'link',
        id: 'entities',
        label: 'Legal entities',
        icon: <Building06Icon />,
        linkConfig: { href: '#entities' },
      },
      {
        kind: 'link',
        id: 'people',
        label: 'Key persons',
        icon: <Users01Icon />,
        linkConfig: { href: '#people' },
      },
      {
        kind: 'link',
        id: 'calendar',
        label: 'Compliance calendar',
        icon: <CalendarIcon />,
        linkConfig: { href: '#calendar' },
      },
      {
        kind: 'group',
        id: 'documents',
        label: 'Documents',
        icon: <FolderIcon />,
        linkConfig: { href: '#documents' },
        children: [
          {
            kind: 'link',
            id: 'formation',
            label: 'Formation',
            icon: <FolderIcon />,
            linkConfig: { href: '#formation' },
          },
          {
            kind: 'link',
            id: 'tax-docs',
            label: 'Tax',
            icon: <FolderIcon />,
            linkConfig: { href: '#tax-docs' },
          },
        ],
      },
    ],
  },
  {
    id: 'apps',
    label: 'My apps',
    collapsible: true,
    items: [
      {
        kind: 'link',
        id: 'indirect-tax',
        label: 'Global indirect tax',
        icon: <CoinsStacked03Icon />,
        linkConfig: { href: '#indirect-tax' },
      },
      {
        kind: 'link',
        id: 'reporting',
        label: 'Tax & reporting',
        icon: <File06Icon />,
        linkConfig: { href: '#reporting' },
      },
      {
        kind: 'link',
        id: 'accounting',
        label: 'Accounting',
        icon: <BookOpen01Icon />,
        linkConfig: { href: '#accounting' },
      },
      {
        kind: 'link',
        id: 'transfer',
        label: 'Transfer pricing',
        icon: <CoinsSwap01Icon />,
        linkConfig: { href: '#transfer' },
      },
      {
        kind: 'link',
        id: 'marketplace',
        label: 'Marketplace',
        icon: <ShoppingBag02Icon />,
        linkConfig: { href: '#marketplace' },
      },
    ],
  },
];

const accountFooter = (collapsed: boolean) => (
  <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
    {[
      {
        label: 'Settings',
        href: '#settings',
        icon: <Settings01Icon size={16} />,
      },
      {
        label: 'Help & support',
        href: '#help',
        icon: <HelpCircleIcon size={16} />,
      },
    ].map((item) => (
      <a
        key={item.href}
        href={item.href}
        aria-label={item.label}
        title={collapsed ? item.label : undefined}
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: 12,
          padding: collapsed ? 6 : '6px 8px',
          minHeight: 28,
          boxSizing: 'border-box',
          color: 'inherit',
          textDecoration: 'none',
          borderRadius: 6,
        }}
      >
        {item.icon}
        {!collapsed && item.label}
      </a>
    ))}
    <Dropdown
      menu={{
        groups: [
          {
            id: 'account',
            items: [
              {
                id: 'profile',
                type: 'link',
                label: 'Your profile',
                linkConfig: { href: '#profile' },
              },
              {
                id: 'organization',
                type: 'link',
                label: 'Organization settings',
                linkConfig: { href: '#organization' },
              },
            ],
          },
        ],
      }}
      trigger={
        <button
          type="button"
          aria-label="Account menu for Sam Schulte"
          style={{
            display: 'flex',
            alignItems: 'center',
            gap: 8,
            padding: 4,
            border: 0,
            background: 'transparent',
            font: 'inherit',
            color: 'inherit',
            textAlign: 'start',
            borderRadius: 6,
          }}
        >
          <Avatar
            identity={{ kind: 'person', userId: 'sam', name: 'Sam Schulte' }}
            size={20}
            decorative
          />
          {!collapsed && 'Sam Schulte'}
        </button>
      }
    />
  </div>
);

export const SideNavCollapsed = () => (
  <div style={{ height: 720 }}>
    <SideNav
      label="Compact workspace"
      activeId="home"
      sections={workspaceSections}
      collapseConfig={{ defaultValue: true }}
      header={(collapsed) =>
        collapsed ? (
          <CommendaBrandIcon size={20} label="Commenda" />
        ) : (
          <CommendaLockupLogo label="Commenda" />
        )
      }
      footer={accountFooter}
    />
  </div>
);
```


Expanded width is 208px; the rail is 56px. Rows are 28px, icons 16px. In the rail, group activation expands navigation to reveal its children; groups with a destination preserve that link, and the Expand sidebar control reveals their descendants. Header/footer render functions prevent a long organization name from overflowing a rail.

## Controlled state [#controlled-state]


```tsx
import { useState } from 'react';

import { BookOpen01Icon } from '@commenda/pathfinder/icons/BookOpen01Icon';
import { Building06Icon } from '@commenda/pathfinder/icons/Building06Icon';
import { CalendarIcon } from '@commenda/pathfinder/icons/CalendarIcon';
import { CheckDone01Icon } from '@commenda/pathfinder/icons/CheckDone01Icon';
import { CoinsStacked03Icon } from '@commenda/pathfinder/icons/CoinsStacked03Icon';
import { CoinsSwap01Icon } from '@commenda/pathfinder/icons/CoinsSwap01Icon';
import { File06Icon } from '@commenda/pathfinder/icons/File06Icon';
import { FolderIcon } from '@commenda/pathfinder/icons/FolderIcon';
import { HomeLineIcon } from '@commenda/pathfinder/icons/HomeLineIcon';
import { Inbox01Icon } from '@commenda/pathfinder/icons/Inbox01Icon';
import { ShoppingBag02Icon } from '@commenda/pathfinder/icons/ShoppingBag02Icon';
import { Users01Icon } from '@commenda/pathfinder/icons/Users01Icon';
import { SideNav, type SideNavSection } from '@commenda/pathfinder/side-nav';

const workspaceSections: SideNavSection[] = [
  {
    id: 'main',
    items: [
      {
        kind: 'link',
        id: 'home',
        label: 'Home',
        icon: <HomeLineIcon />,
        linkConfig: { href: '#home' },
      },
      {
        kind: 'link',
        id: 'tasks',
        label: 'Tasks',
        icon: <CheckDone01Icon />,
        count: 4,
        linkConfig: { href: '#tasks' },
      },
      {
        kind: 'link',
        id: 'inbox',
        label: 'Inbox',
        icon: <Inbox01Icon />,
        count: 3,
        unread: true,
        linkConfig: { href: '#inbox' },
      },
    ],
  },
  {
    id: 'overview',
    label: 'Overview',
    collapsible: true,
    items: [
      {
        kind: 'link',
        id: 'entities',
        label: 'Legal entities',
        icon: <Building06Icon />,
        linkConfig: { href: '#entities' },
      },
      {
        kind: 'link',
        id: 'people',
        label: 'Key persons',
        icon: <Users01Icon />,
        linkConfig: { href: '#people' },
      },
      {
        kind: 'link',
        id: 'calendar',
        label: 'Compliance calendar',
        icon: <CalendarIcon />,
        linkConfig: { href: '#calendar' },
      },
      {
        kind: 'group',
        id: 'documents',
        label: 'Documents',
        icon: <FolderIcon />,
        linkConfig: { href: '#documents' },
        children: [
          {
            kind: 'link',
            id: 'formation',
            label: 'Formation',
            icon: <FolderIcon />,
            linkConfig: { href: '#formation' },
          },
          {
            kind: 'link',
            id: 'tax-docs',
            label: 'Tax',
            icon: <FolderIcon />,
            linkConfig: { href: '#tax-docs' },
          },
        ],
      },
    ],
  },
  {
    id: 'apps',
    label: 'My apps',
    collapsible: true,
    items: [
      {
        kind: 'link',
        id: 'indirect-tax',
        label: 'Global indirect tax',
        icon: <CoinsStacked03Icon />,
        linkConfig: { href: '#indirect-tax' },
      },
      {
        kind: 'link',
        id: 'reporting',
        label: 'Tax & reporting',
        icon: <File06Icon />,
        linkConfig: { href: '#reporting' },
      },
      {
        kind: 'link',
        id: 'accounting',
        label: 'Accounting',
        icon: <BookOpen01Icon />,
        linkConfig: { href: '#accounting' },
      },
      {
        kind: 'link',
        id: 'transfer',
        label: 'Transfer pricing',
        icon: <CoinsSwap01Icon />,
        linkConfig: { href: '#transfer' },
      },
      {
        kind: 'link',
        id: 'marketplace',
        label: 'Marketplace',
        icon: <ShoppingBag02Icon />,
        linkConfig: { href: '#marketplace' },
      },
    ],
  },
];

export const SideNavControlled = () => {
  const [collapsed, setCollapsed] = useState(false);
  const [expanded, setExpanded] = useState<readonly string[]>([
    'overview',
    'documents',
  ]);

  return (
    <div style={{ height: 480 }}>
      <SideNav
        label="Controlled workspace"
        activeId="formation"
        sections={workspaceSections}
        collapseConfig={{ value: collapsed, onValueChange: setCollapsed }}
        expansionConfig={{ value: expanded, onValueChange: setExpanded }}
      />
      <p>
        {collapsed ? 'Rail' : 'Expanded'} · {expanded.length} expanded groups
      </p>
    </div>
  );
};
```


Expansion and collapse are independent of the active route. The app may persist them per user; Pathfinder does not read browser storage. Overflow fades follow actual scroll position. Use an app-owned mobile navigation surface when the persistent sidebar does not fit; the component does not guess viewport breakpoints or mutate routes. Chat conversation history remains a separately composed pattern. Read the [hierarchy rules](/docs/components/side-nav#design-rules).

## Design rules [#design-rules]

Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references.

### Side navigation [#side-navigation]

- **NAV-001:** Expanded 208px, rail 56px, pearl100 background, 12px inset, 24px section gaps. Header mark/wordmark and collapse control; optional Home/Chat SegmentedControl; footer account composition. Rows 28px, radius 6, horizontal inset 8, gap 12, icons 16.
- **NAV-002:** Label/13 pearl900; icon pearl800. Hover pearl200, selected pearl300, selected label1000 and icon blue700. The landing link and expansion chevron share one continuous hover/selected row surface. Weight never changes. The docs prose mentions selected 200, but the reviewed component 714:15 uses 300; the implementation follows the component. Count Label/12, optional unread dot; rail uses tooltip and accessible metadata.
- **NAV-003:** Stable globally distinct IDs, one active destination, real anchors, separate group chevrons. Routing, auth, persistence and fetching belong to the application. Collapse retains group/section state. Chat history is a separate composed pattern; do not embed an application’s routes or conversation API in SideNav.
- **NAV-004:** Scroll fades match CommendaNavbar: 48px from the Soft Pearl 100 surface (#FCFBF8) to transparent, appearing only at overflowing edges. The parent supplies available height. No global keyboard shortcut, cookie or workspace provider is installed by the component.
