Navigation

The bar along the top, the column of sections at the side, and the pages of a section beside it.

Try it

The menu button collapses the side nav; with the narrow preview on, it opens the nav as a drawer over the page. Press a page to move there.

Two levels

SideNav is the primary navigation, drawn 200 wide and 80 with icons only: a link per item with a 24 icon and a Semibold label, the current one on surface/base in the accent, and a group at the foot for Settings. Collapsed, every item keeps its name in a Tooltip. SideNavSecondary is the second level, the pages of the section the reader is in: sections under captions that fold, each a details as the Accordion’s item, so folding is the platform’s; 240 wide, or a 24 strip holding the drawn collapse button. They are two components because their anatomy and the way they collapse differ; one API with a level would carry props that serve one of them.

The control is the top bar’s

The drawing puts the menu button in the TopBar, and that is the control: on a wide screen it collapses the side nav, on a narrow one it opens it. onMenu draws the button; the caller holds collapsed and open and decides which the press means. The side nav has no button of its own; the second level has the drawn one on its strip.

On a narrow screen

Below narrow, media.down.md, below 768, unless told, the side nav is a modal dialog sliding from the start side: the top layer, the scrim, the inert page, Esc and the focus going back are the platform’s, as the Dialog’s. onClose is called and the caller sets open to false. In the drawer the labels are always shown, whatever collapsed says. A press on a link does not close it: the caller closes on navigation, as its router tells it, which is what this page does. The query is a prop so a product can move the line; its default is the breakpoint scale’s md.

Phone and tablet top bars are drawn, and a phone bottom bar, and the package has built none of them yet (this paragraph said the drawing had no phone version; corrected 2026-09-24, fidelity audit). Until it does, on a narrow screen the caller keeps the actions that fit, an overflow menu for the rest, and collapses or leaves out the second level, whose pages are then reached from the page itself.

Where it departs from the drawing

The second level’s captions are drawn in text/disabled; here they are text/tertiary, because a caption is information and has to read. The site’s own nav keeps its rail with captions, the Material 3 site’s shape; the package takes the product’s.

Accessibility

Each is a nav with a name, Main and Section unless told, so a reader can jump between them; the current page carries aria-current, which the caller sets. The top bar is the page’s banner. The menu button says what it controls with aria-expanded. A folded section is a details: Enter and Space and the state a reader hears are the platform’s, and text in a folded section is found by the browser’s own search.

Using it

TSX
import { TopBar, SideNav, SideNavSecondary, Button, Avatar } from 'alpenglow';

const [collapsed, setCollapsed] = useState(false);
const [open, setOpen] = useState(false);
const narrow = useMediaQuery(SIDE_NAV_NARROW); // the app's own hook, or any

<TopBar
  brand={<Logo />}
  onMenu={() => (narrow ? setOpen(true) : setCollapsed((c) => !c))}
  menuExpanded={narrow ? open : !collapsed}
  actions={<><Button iconStart={<Add />}>Create</Button><Avatar name="Helen Hernandez" /></>}
/>
<div style={{ display: 'flex', minHeight: '100dvh' }}>
  <SideNav
    items={items}
    footer={[{ href: '/settings', label: 'Settings', icon: <Settings /> }]}
    renderLink={(props) => <NextLink {...props} />}
    collapsed={collapsed}
    open={open}
    onClose={() => setOpen(false)}
  />
  <SideNavSecondary sections={sections} collapsed={aside} onCollapsedChange={setAside} />
  <main>…</main>
</div>
SideNav props
PropTypeDefault
itemsSideNavItem[] — { href, label, icon, current? }required
footerSideNavItem[]—
renderLink(props) => ReactNodean a
aria-labelstring'Main'
collapsedbooleanfalse
open, onCloseboolean, () => void — narrow onlyfalse
narrowstring — a media querymedia.down.md — '(width < 48rem)'
closeLabel, classNamestring'Close'
SideNavSecondary props
PropTypeDefault
sectionsSideNavSection[] — { label, items, defaultOpen? }required
renderLink(props) => ReactNodean a
aria-labelstring'Section'
collapsed, onCollapsedChangeboolean, (next) => voidfalse
collapseLabel, expandLabelstring'Collapse navigation', 'Expand navigation'
classNamestring—
TopBar props
PropTypeDefault
brandReactNode—
onMenu() => void — draws the menu button—
menuLabel, menuExpandedstring, boolean'Menu', —
childrenReactNode — the middle—
actionsReactNode — the end—
classNamestring—