Sidebar
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 codeHide 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
brandand 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 viaaria-controls(a generated id) andaria-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), togglearia-expanded="false". - Mobile, open — clicking the toggle sets the nav’s
data-open="true"and the toggle’saria-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 codeHide 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 codeHide 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 codeHide 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 codeHide 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 carriesaria-controls(pointing at the nav’s id),aria-expanded, andaria-label="Toggle navigation". - Nav links are plain
<a>elements in normal tab order. - Visible
focus-visiblering 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
brandfor 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.