Skip to main content

SideNav

Organismbeta@macaw-ds/ui · since 0.1.0

The product's primary navigation in the left column of AppShell. Not for sections within a page; that is TabNav.

Import

import { SideNav, SideNavGroup, SideNavItem } from '@macaw-ds/ui';

Props

PropTypeDefaultDescription
aria-label*stringMain

Names the navigation landmark, e.g. "Main".

titlestringBilling

On SideNav.Group: the group heading.

activebooleanfalse

On SideNav.Item: the current page.

Accessibility: Sets aria-current="page".

iconReactNode—

On SideNav.Item: a decorative Icon before the label.

asChildbooleanfalse

On SideNav.Item: render a router Link instead of an anchor.

childrenReactNode—

SideNav.Group elements.

Slots

Named parts you pass content into.

  • Group
  • Item

Tokens used

Every value this component renders comes from one of these tokens.

TokenLightDark
color.status.accent-bg

semantic · color

oklch(0.97 0.02 262)
oklch(0.24 0.08 262)
color.status.accent-fg

semantic · color

oklch(0.38 0.15 262)
oklch(0.88 0.08 262)
color.fg.secondary

semantic · color

oklch(0.38 0.012 250)
oklch(0.87 0.007 250)
color.fg.primary

semantic · color

oklch(0.15 0.012 250)
oklch(0.985 0.002 250)
color.fg.muted

semantic · color

oklch(0.48 0.012 250)
oklch(0.66 0.01 250)
color.surface.layer-1-hover

semantic · color

oklch(0.965 0.003 250)
oklch(0.21 0.012 250)
size.control.sm

semantic · dimension

28px
28px
text.size.label-sm

semantic · dimension

0.75rem
0.75rem
text.size.caption

semantic · dimension

0.6875rem
0.6875rem
color.focus.ring

semantic · color

oklch(0.51 0.2 262)
oklch(0.68 0.16 262)

Eject

Ejecting copies the source into your repo and records it in .macaw/ejected.json, so the fork stays visible. The same file is served as a shadcn-compatible registry item.

With the MACAW CLI
npx @macaw-ds/cli add side-nav --eject
With shadcn
npx shadcn@latest add https://macaw-design-system.vercel.app/r/side-nav.json

Bundle budget: 2,000 bytes gzipped, enforced by size-limit in CI.

Manifest

registry/manifests/side-nav.json, the contract the playground, CLI, plugin and MCP server read.

{
  "name": "SideNav",
  "slug": "side-nav",
  "package": "@macaw-ds/ui",
  "status": "beta",
  "since": "0.1.0",
  "level": "organism",
  "category": "navigation",
  "purpose": "The product's primary navigation in the left column of AppShell. Not for sections within a page; that is TabNav.",
  "description": "Groups of links with optional headings and icons. One item is active and carries aria-current=\"page\". Item height follows the density mode. AppShell gives it a full-height, independently scrolling column on surface layer-1.",
  "exports": [
    "SideNav",
    "SideNavGroup",
    "SideNavItem"
  ],
  "props": {
    "aria-label": {
      "type": "string",
      "default": "Main",
      "required": true,
      "description": "Names the navigation landmark, e.g. \"Main\"."
    },
    "title": {
      "type": "string",
      "default": "Billing",
      "required": false,
      "description": "On SideNav.Group: the group heading.",
      "control": "none"
    },
    "active": {
      "type": "boolean",
      "default": false,
      "required": false,
      "description": "On SideNav.Item: the current page.",
      "a11y": "Sets aria-current=\"page\"."
    },
    "icon": {
      "type": "node",
      "required": false,
      "description": "On SideNav.Item: a decorative Icon before the label.",
      "control": "none"
    },
    "asChild": {
      "type": "boolean",
      "default": false,
      "required": false,
      "description": "On SideNav.Item: render a router Link instead of an anchor.",
      "control": "none"
    },
    "children": {
      "type": "node",
      "required": false,
      "description": "SideNav.Group elements.",
      "control": "none"
    }
  },
  "slots": [
    "Group",
    "Item"
  ],
  "tokens": [
    "color.status.accent-bg",
    "color.status.accent-fg",
    "color.fg.secondary",
    "color.fg.primary",
    "color.fg.muted",
    "color.surface.layer-1-hover",
    "size.control.sm",
    "text.size.label-sm",
    "text.size.caption",
    "color.focus.ring"
  ],
  "a11y": {
    "role": "navigation, containing lists of links",
    "focusable": true,
    "keyboard": [
      "Tab: move between items (each is a link)",
      "Enter: follow the link"
    ],
    "name": "The landmark takes its name from aria-label; each group list from its visible title (aria-labelledby); each item from its text. Group titles are not headings, so they do not interrupt the page outline.",
    "notes": [
      "The active item has aria-current=\"page\" and a tinted background plus accent text, so selection is not shown by colour of one property alone.",
      "Icons are decorative; the label always names the item."
    ]
  },
  "motion": {
    "description": "Item background and text colour ease on hover.",
    "tokens": [
      "duration.fast",
      "ease.standard"
    ]
  },
  "budget": {
    "gzip": 2000
  },
  "rules": [
    "Exactly one item is active, and it matches the current route.",
    "Group related destinations under a short heading; keep fewer than ten items per group.",
    "With a router, pass the Link through asChild so client-side navigation keeps working.",
    "Place SideNav in AppShell's sideNav slot; it does not set its own width or background."
  ],
  "dos": [
    "Use icons for every item in a group or for none of them."
  ],
  "donts": [
    "Do not put actions (buttons) in SideNav; it is for navigation only.",
    "Do not nest groups."
  ],
  "examples": [
    {
      "name": "Default",
      "props": {
        "aria-label": "Main"
      }
    }
  ],
  "registryDependencies": [],
  "dependencies": [
    "radix-ui"
  ],
  "related": [
    "app-shell",
    "tab-nav",
    "top-bar"
  ]
}