Pho Design System

Sidebar

The app navigation rail — Pho’s first section: a composed assembly built from the micro components, not another primitive. It encodes the rail used across Photon surfaces (this docs site, the dashboard): a sticky full-height column with a brand header, a scrollable stack of titled nav groups, and a pinned footer that usually carries the account row.

Sections stay presentational. No router, no data fetching, no theme plumbing — links render through the host’s LinkProvider (the same contract ButtonLink uses), the current page arrives as an active prop, and interactive parts compose with other Pho components (the account row slots into a Menu.Trigger).

Import

import { Sidebar } from "@photon-ai/pho-ui/sections/sidebar";

Anatomy

<Sidebar.Root>
  <Sidebar.Header>
    <PhotonMark className="text-pho-brand size-6" />
    <span className="font-medium tracking-tight">Photon</span>
  </Sidebar.Header>

  <Sidebar.Nav>
    <Sidebar.Group>
      <Sidebar.GroupLabel>Pages</Sidebar.GroupLabel>
      <Sidebar.List>
        <Sidebar.Item href="/" icon={IconLayoutDashboard} active>
          Overview
        </Sidebar.Item>
        <Sidebar.Item href="/members" icon={IconUsers}>
          Members
        </Sidebar.Item>
      </Sidebar.List>
    </Sidebar.Group>
  </Sidebar.Nav>

  <Sidebar.Footer>
    <Sidebar.List>
      <Sidebar.Item icon={IconBook} href="https://photon.codes/docs/" external>
        Docs
      </Sidebar.Item>
    </Sidebar.List>

    <Menu.Root>
      <Menu.Trigger render={<Sidebar.AccountRow name="Ada Lovelace" />} />
      <Menu.Portal>{/* account menu */}</Menu.Portal>
    </Menu.Root>
  </Sidebar.Footer>
</Sidebar.Root>

Items

Sidebar.Item renders a link through the host LinkProvider when href is given, and a plain button otherwise. active lifts the row onto the primary surface with the site hairline and sets aria-current="page"; external opens a new tab and appends a ↗ cue (with a screen-reader note).

Sidebar.Submenu is a row that holds rows. The trigger wears Item metrics with a trailing chevron that turns as the panel opens; the children are ordinary Sidebar.Items (no icon needed), indented so their labels line up with the parent’s label. The fold is the library’s Collapsible — one height animation, collapsed under reduced motion — and a closed group adds no height, so siblings never shift.

Children report active upward: while one is current the parent takes primary ink (label and icon, no surface — the child carries the chip), and with openWhenChildActive (the default) the group opens on load without animating, and again whenever a different child becomes current. The user can still collapse it.

<Sidebar.List>
  <Sidebar.Item href="/" icon={IconLayoutDashboard}>
    Overview
  </Sidebar.Item>
  <Sidebar.Submenu label="Platforms" icon={IconMessage}>
    <Sidebar.Item href="/platforms/sms">SMS</Sidebar.Item>
    <Sidebar.Item href="/platforms/imessage" active>
      iMessage
    </Sidebar.Item>
    <Sidebar.Item href="/platforms/email">Email</Sidebar.Item>
  </Sidebar.Submenu>
  <Sidebar.Item href="/webhooks" icon={IconWebhook}>
    Webhooks
  </Sidebar.Item>
</Sidebar.List>

Leave it uncontrolled (defaultOpen) in a route-based rail — the active child does the opening. Pass open + onOpenChange to own the state, for instance to persist collapsed groups; the auto-open then arrives as an onOpenChange(true) for the host to apply.

Columns

For a long, flat registry — like this site’s component list — List lays rows out in a two-column grid with columns.

<Sidebar.List columns>
  <Sidebar.Item href="/components/button">Button</Sidebar.Item>
  <Sidebar.Item href="/components/input">Input</Sidebar.Item>
  {/* … */}
</Sidebar.List>

Account row

Sidebar.AccountRow is ONE button — the ⋯ circle on the right is decorative (a span wearing the stock secondary-circle button classes). The row is the control, the circle is the indicator: hovering or pressing anywhere on the row lifts and squeezes the ⋯ circle (driven by the row’s group state) while the row itself stays quiet, so the whole chip reads as a single control without nesting buttons. It forwards every prop, so it slots straight into Base UI’s render prop:

<Menu.Root>
  <Menu.Trigger
    render={<Sidebar.AccountRow name="Ada Lovelace" avatarSrc={pictureUrl} />}
  />
  <Menu.Portal>
    <Menu.Positioner side="top" align="end">
      <Menu.Popup>{/* settings / theme / log out */}</Menu.Popup>
    </Menu.Positioner>
  </Menu.Portal>
</Menu.Root>

data-popup-open and data-pressed from the trigger drive the ⋯ circle: it stays lifted while the menu is open and scales down slightly on press — the row keeps only its keyboard focus ring; hover, press, and menu-open feedback all land on the circle.

Props

Sidebar.Item

Sidebar.Submenu

Sidebar.List

Sidebar.AccountRow

Every part merges className onto its root and forwards native attributes.

Accessibility

Best practices