Deck
Browse the catalog

Build / Organisms

SideNav

  • organism
  • play-fn

The desktop navigation rail.

BottomNav's counterpart above the md breakpoint, and a deliberate sibling of it rather than a port: the two carry different destinations because the devices are used differently. The rail leads with managing your own cards and connections; the bar leads with Exchange, which is a thing you do standing in front of someone with a phone in your hand.

The root is a <nav>, not the <aside> the visual design implies. <aside> maps to the complementary landmark — "tangentially related to the main content" — which the app's own navigation is not. The rail look comes from CSS; the semantics stay honest.

footerItems renders as a second list inside that same landmark, pinned down by a flexible margin. Splitting it into its own <nav> would either add an unnamed landmark or a second one competing for the same name, for a result CSS already gives us.

A destination with an href renders as a real link; without one it renders as a button. onSelect fires either way, so a router can intercept the click while the markup stays navigable and shareable. The active item carries aria-current="page", so it is announced rather than only colored.

import { SideNav } from '@meetcard/deck'

Example

<SideNav currentId="/connections" items={destinations} footerItems={[settings]} />

Stories

Every preview below is the real component, rendered in this page from the same story Storybook runs. The width control resizes each preview's container; CSS media queries still follow the browser window, so breakpoint behaviour is worth checking in Storybook.

Default
Source
const Default = () => <SideNav
    items={[
      { id: '/', href: '/', label: 'Dashboard', icon: <Icon d="M4 11l8-7 8 7v8a1 1 0 01-1 1h-5v-6h-4v6H5a1 1 0 01-1-1z" /> },
      { id: '/cards', href: '/cards', label: 'My cards', icon: <Icon d="M3 6h18v12H3zM3 10h18M7 14h4" /> },
      { id: '/connections', href: '/connections', label: 'Connections', icon: <Icon d="M4 19v-1a4 4 0 014-4h2a4 4 0 014 4v1M9 10a3 3 0 100-6 3 3 0 000 6zM17 11a3 3 0 100-6" /> },
      { id: '/events', href: '/events', label: 'Events', icon: <Icon d="M4 6h16v14H4zM4 10h16M8 3v4M16 3v4" /> },
    ]}
    currentId="/" />;

Settings pinned to the foot. It shares the rail's landmark rather than forming a second one — the assertion below is what keeps it that way.

With Pinned Footer
Source
const WithPinnedFooter = () => <SideNav
    items={[
      { id: '/', href: '/', label: 'Dashboard', icon: <Icon d="M4 11l8-7 8 7v8a1 1 0 01-1 1h-5v-6h-4v6H5a1 1 0 01-1-1z" /> },
      { id: '/cards', href: '/cards', label: 'My cards', icon: <Icon d="M3 6h18v12H3zM3 10h18M7 14h4" /> },
      { id: '/connections', href: '/connections', label: 'Connections', icon: <Icon d="M4 19v-1a4 4 0 014-4h2a4 4 0 014 4v1M9 10a3 3 0 100-6 3 3 0 000 6zM17 11a3 3 0 100-6" /> },
      { id: '/events', href: '/events', label: 'Events', icon: <Icon d="M4 6h16v14H4zM4 10h16M8 3v4M16 3v4" /> },
    ]}
    currentId="/connections"
    footerItems={[
      { id: '/settings', href: '/settings', label: 'Settings', icon: <Icon d="M4 6h16M4 12h16M4 18h16M9 4v4M15 10v4M9 16v4" /> },
    ]} />;

A count trails the label, where the rail has room for it.

With Badge
Source
const WithBadge = () => <SideNav
    items={destinations.map((d) =>
      d.id === '/connections' ? { ...d, badge: 3 } : d,
    )}
    currentId="/"
    footerItems={[
      { id: '/settings', href: '/settings', label: 'Settings', icon: <Icon d="M4 6h16M4 12h16M4 18h16M9 4v4M15 10v4M9 16v4" /> },
    ]} />;

Props

PropTypeDefaultDescription
itemsrequiredSideNavItemProps[]——
currentIdstring—id of the active destination.
footerItemsSideNavItemProps[]—Destinations pinned to the foot of the rail, below a flexible gap — Settings in MeetCard. They are the same kind of thing as items, only reached less often, so they render inside the same navigation landmark as a second list rather than in a landmark of their own.
labelstring'Global'Accessible name of the landmark. Deliberately not "Primary": the shell renders this and BottomNav into the same document, and two navigation landmarks may not share a name.
onSelect(id: string) => void——

Also accepts everything from Omit, so id, data-* and event handlers pass straight through.

Open in Storybookfor controls, the a11y panel and the interaction runner.