A vertical navigation aside, sticky and full-height on desktop, collapsible behind a toggle on mobile.

Last updated

Uses JavaScript

A small island toggles data-open on the nav and aria-expanded on the button when the mobile toggle is clicked, showing or hiding the nav below the header row; a sibling CSS rule also reveals the footer once the nav is open. With overlay the same island moves the nav into a <dialog> drawer and back on close.

Without JS: The toggle is hidden (`no-js:hidden`) and the nav stays expanded (`no-js:flex`), and the footer stays expanded too (`no-js:block`) — so below the `md:` breakpoint everything is reachable without a working control to click. Desktop is unaffected: the nav is always visible above the `md:` breakpoint, shown by CSS alone.

Show code
<Sidebar>
<a slot="brand" href="/" class="font-semibold">Larkspur</a>
<a href="/dashboard">Dashboard</a>
<a href="/projects">Projects</a>
<a href="/settings">Settings</a>
<div slot="footer">v1.2.0</div>
</Sidebar>

Resize below md (768px) to see the mobile header + toggle; the nav and footer behave differently at that width (see Anatomy).

Add

npx @ava-ui/apogee add sidebar

Ships @lucide/astro (for the toggle’s Menu icon). No Apogee component dependencies.

Usage

---
import Sidebar from '@/components/apogee/Sidebar/Sidebar.astro'
---
<Sidebar>
  <a slot="brand" href="/">Larkspur</a>
  <a href="/dashboard">Dashboard</a>
  <a href="/settings">Settings</a>
</Sidebar>

Anatomy

  • Header row — always visible; holds brand and the mobile toggle. On desktop the toggle is hidden (md:hidden) and the row loses its bottom border.
  • brand (slot="brand") — logo or product name, left-aligned in the header row.
  • Nav (default slot) — links or NavLink-like items, stacked with a 2px gap. Collapsed by default on mobile; always shown on desktop.
  • footer (slot="footer") — pinned to the bottom on desktop (mt-auto). On mobile with JS, hidden until the nav is opened, then revealed via a sibling CSS rule. Without JS (no-js:block) it’s always shown.
  • Toggle button — auto-rendered, md:hidden. Controls the nav via aria-controls (a generated id) and aria-expanded.

API

@composable — exposed to Content Editors in AVA. Any other <aside> attribute passes through.

Prop Type Default Description
variant 'default' | 'slim' 'default' slim is a 4rem icon rail from md: NavLink labels become screen-reader only, links centre. Below md it behaves like the default.
overlay boolean false On mobile, the nav opens as a start-edge drawer over the page (a native <dialog>) instead of pushing content down. Without JS the inline panel remains.
toggleLabel string 'Toggle navigation' Name of the phone toggle button.
overlayLabel string 'Navigation' Name of the overlay drawer.
class string — Extra classes on the root <aside>, merged last.

States

  • Mobile, closed (default) — nav hidden (display: none), toggle aria-expanded="false".
  • Mobile, open — clicking the toggle sets the nav’s data-open="true" and the toggle’s aria-expanded="true"; the nav becomes a flex column below the header.
  • Desktop (md: and up) — nav always visible (md:flex); toggle hidden.
  • Overlay open (mobile, overlay) — the nav and footer live inside an open <dialog>; Escape, the close button or the backdrop close it and move them back. Resizing to desktop while open closes it.

Examples

Nav links with an icon, and a footer used for a persistent action:

Show code
<Sidebar>
<a slot="brand" href="/" class="font-semibold">Larkspur</a>
<a href="/dashboard">Dashboard</a>
<a href="/settings" class="flex items-center gap-2"><Settings class="size-4" /> Settings</a>
<div slot="footer" class="text-sm text-neutral-faded">Signed in as jane@example.com</div>
</Sidebar>

Groups, section headings and a featured card in the footer. NavGroup is a native <details> that opens when it contains the current page; headings are plain text; a Divider separates sections.

Show code
<Sidebar>
<a slot="brand" href="/">Larkspur</a>
<NavLink href="/dashboard" match="prefix"><Home slot="start" />Dashboard</NavLink>
<NavGroup label="Projects">
  <FolderKanban slot="start" />
  <NavLink href="/projects/spring">Spring launch</NavLink>
  <NavLink href="/projects/site">Website</NavLink>
</NavGroup>
<Divider inset={2} />
<p class="px-3 pb-1 text-xs font-semibold uppercase tracking-wide text-neutral-faded">Team</p>
<NavLink href="/members"><Users slot="start" />Members <Badge slot="end" size="small">4</Badge></NavLink>
<NavLink href="https://help.example.com"><LifeBuoy slot="start" />Help centre</NavLink>
<Dismissible slot="footer" label="Dismiss">
  <Card padding="small">…<Progress value={60} label="Storage" />…</Card>
</Dismissible>
</Sidebar>

Slim rail

An icon-only rail from md. Give every link a start icon; wrap it in a Tooltip for a visible name on hover and focus (the label stays in the accessibility tree). A Popover on a rail item is the flyout for a group. Its trigger takes the NavLink classes (navLinkClass, navLinkIdle) and an apogee-navlink-label, so it shows its label on phones and goes icon-only on the rail like the links:

Show code
<Sidebar variant="slim">
<a slot="brand" href="/" aria-label="Larkspur">A</a>
<Tooltip text="Dashboard" placement="end"><NavLink href="/dashboard"><Home slot="start" />Dashboard</NavLink></Tooltip>
<Tooltip text="Inbox" placement="end"><NavLink href="/inbox"><Inbox slot="start" />Inbox</NavLink></Tooltip>
<Popover id="rail-projects" placement="end">
  <button slot="anchor" popovertarget="rail-projects" class={`${navLinkClass} ${navLinkIdle}`}><FolderKanban class="size-4" /><span class="apogee-navlink-label">Projects</span></button>
  <NavLink href="/projects/spring">Spring launch</NavLink>
  <NavLink href="/projects/site">Website</NavLink>
</Popover>
</Sidebar>

Overlay

On a phone the nav opens as a drawer over the page. Resize below md and press the toggle:

Show code
<Sidebar overlay>
<a slot="brand" href="/">Larkspur</a>
<NavLink href="/dashboard"><Home slot="start" />Dashboard</NavLink>
…
</Sidebar>

Accessibility

Key Action
Tab Moves through the toggle and the nav links in order.
Enter Space On the toggle, open or close the nav.
Esc With overlay, closes the drawer and returns focus to the toggle.
  • Toggle is a real <button>. It carries aria-controls (pointing at the nav’s id), aria-expanded, and aria-label="Toggle navigation".
  • Nav links are plain <a> elements in normal tab order.
  • Visible focus-visible ring on the toggle.
  • No JS: the toggle is no-js:hidden, and the nav (no-js:flex) and footer (no-js:block) both stay expanded — so on a small viewport without JavaScript, the nav and every link in it, plus the footer, are reachable without a working control. Desktop viewports are unaffected, since the nav is always shown there by CSS alone.
  • With overlay, the drawer is a native modal <dialog> (focus trapped, Escape closes, focus returns to the toggle) labelled “Navigation”. The nav is moved, not copied, so there is never a duplicate landmark.

Tokens

Token Used for
--border-neutral Aside end border (desktop), header bottom border (mobile), footer top border
--background-page Aside background
--foreground-neutral Toggle icon colour
--background-neutral-faded-highlighted Toggle hover background
--outline-color-focus Toggle focus ring

Do / Don’t

  • Do put the most-used links first — they’re always visible on desktop, and stay expanded on mobile without JS too.
  • Do use brand for a logo or product name only, not another nav item.
  • Don’t worry about the mobile toggle as a hard dependency — without JS it’s hidden and the nav/footer fall back to expanded, so content stays reachable either way.
  • Don’t put page content in footer — it’s for persistent bits (account switcher, version, sign out); on mobile with JS it only shows once the nav is opened.

Content

  • Nav labels: nouns, sentence case, one to three words (“Dashboard”, “Team settings”).
  • Footer content stays short — a name, plan, or single action; it’s cramped in the rail.