AppSidebar
AppSidebar keeps an application's destinations in view. Choose its children door when links must
keep their router behavior; groups supports links, router links and button actions.
Examples
Composed links and an icon rail
Pass SidebarGroup children when your app owns routing. On collapse, the sidebar becomes a 64px rail and supplies native tooltips and accessible names to its link descendants from their text.
Convenience groups
groups renders an anchor when an item has href, composes a router link with asChild, and
keeps a button when neither is supplied. Its string label names the icon rail item.
When to use
Use AppSidebar for grouped, persistent destinations in an application shell.
Reach for something else when:
- A short row of site-level routes is enough — use Navbar.
- The destinations are peer views inside the current page — use Tabs.
- You only need one navigation section — use SidebarGroup.
Width and Shell ownership
CSS-only AppSidebar markup defaults to 260px; add lyra-appsidebar--rail for its 64px rail
default. Set --appsidebar-width when your application needs a different width. When AppSidebar is
the direct sidebar child of a content-scroll Shell, AppSidebar owns that rail; Shell.sidebarWidth
continues to size ordinary sidebar content and page-scroll Shell rails.
<!-- Defaults to 260px. -->
<nav class="lyra-appsidebar" aria-label="Workspace navigation"></nav>
<!-- Defaults to 64px; this explicit property is also supported. -->
<nav
class="lyra-appsidebar lyra-appsidebar--rail"
style="--appsidebar-width: 64px"
aria-label="Workspace navigation"
></nav>
<!-- A consumer override for an expanded sidebar. -->
<nav
class="lyra-appsidebar"
style="--appsidebar-width: 296px"
aria-label="Workspace navigation"
></nav>Accessibility
- AppSidebar renders a native
<nav>. Passaria-labelthrough to it when the page has more than one navigation landmark. - In
groupsmode,hrefitems are native links,asChilditems preserve the router link, and action-only items remain buttons. The active item receivesaria-current="page". The item callback runs beforeonSelect(id, item). Usetarget="_blank"for native new-tab links. - In composition mode, children remain their original elements. Use real anchors for routes; when
rail mode is active, link descendants get a
titleandaria-labelfrom an explicit label, title, or their text content. - The collapse control is a native button whose accessible name and tooltip come from
labels(English by default). In rail mode, group labels, item text and badges are visually hidden, while the control and item names remain available. - A WorkspaceSwitcher in the brand slot becomes a compact avatar trigger in rail mode. Its workspace name remains available to assistive technology; its popover keeps a usable width and stays within a narrow viewport.
React Router and Next.js destinations
// React Router: the router owns client-side navigation.
<AppSidebar groups={[{ items: [{ id: 'reports', label: 'Reports', active: true,
asChild: <Link to="/reports" /> }] }]} />
// Next.js: the same item contract accepts Next Link.
<AppSidebar groups={[{ items: [{ id: 'billing', label: 'Billing',
asChild: <Link href="/billing" /> }] }]} />A plain route needs only href: '/reports'. Set target: '_blank' and
rel: 'noopener noreferrer' for a new tab. asChild replaces the child link's content with
the item's label and icon; do not put a second label inside the Link.
API and code
| Name | Type | Required | Description |
|---|---|---|---|
brand | ReactNode | — | Optional brand content placed above the navigation groups. |
groups | AppSidebarGroup[] | — | Convenience data mode. Each group is composed through {@link SidebarGroup}; destinations render as links, while items without destinations remain buttons. |
footer | ReactNode | — | Optional utility links or user content separated below the navigation groups. |
width | number | — | Sidebar width in pixels while expanded. Default: `260`. In a content-scroll {@link Shell} where AppSidebar is the direct sidebar child, this width owns that rail. |
collapsible | boolean | — | Whether to render a control that switches between expanded and icon-rail modes. |
collapsed | boolean | — | Controlled icon-rail state. |
defaultCollapsed | boolean | — | Initial icon-rail state when uncontrolled. |
onCollapsedChange | (collapsed: boolean) => void | — | Called whenever the icon-rail state changes. |
onSelect | (id: string, item: AppSidebarGroupItem) => void | — | Called after an item-level callback in data mode. |
labels | AppSidebarLabels | — | Localized labels for the collapse control. |
children | ReactNode | — | Composition mode. Pass {@link SidebarGroup} children (including link children) to preserve their original element type and routing behavior. |
x-data="lyraAppSidebar({ … })"
| Option | Type | Required | Description |
|---|---|---|---|
defaultCollapsed | boolean | — | Whether the sidebar starts in its icon-rail state. Default: `false`. |
width | number | — | Sidebar width in pixels while expanded. Default: `260`. |
labels | LyraAppSidebarLabels | — | Localized labels for the collapse control. |
The composed-link door is ordinary navigation markup. Supply the collapse behavior, rail labels and
selection handling yourself outside React. Render the collapse control as a native button with an
aria-label; inject a chevron <svg> and flip it between the expanded and rail states. If you leave
the button empty, the stylesheet supplies a fallback chevron so the control never renders as a blank
box, but an injected icon lets you reflect collapse state:
<nav class="lyra-appsidebar" aria-label="Workspace navigation">
<div class="lyra-appsidebar__brand"><strong>Acme</strong></div>
<div class="lyra-appsidebar__groups">
<div class="lyra-sbgroup">
<div class="lyra-sbgroup__label">Workspace</div>
<div class="lyra-sbgroup__items">
<a
class="lyra-sbgroup__item lyra-sbgroup__item--active"
href="/overview"
aria-current="page"
>
<span class="lyra-sbgroup__item-label">Overview</span>
</a>
</div>
</div>
</div>
<button class="lyra-appsidebar__toggle" type="button" aria-label="Collapse sidebar">
<svg
aria-hidden="true"
width="15"
height="15"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<!-- expanded: chevron points inward; swap to m9 18 6-6-6-6 in the rail -->
<path d="m15 18-6-6 6-6" />
</svg>
</button>
</nav><lyra:app-sidebar> Generated from lyra-ds/blade v0.10.0.
The behavior comes from lyraAppSidebar() — install @lyra-ds/alpine and see the HTML + Alpine tab.
| Prop | Default | Required | Example values |
|---|---|---|---|
brand | null | — | — |
groups | [] | — | — |
footer | null | — | — |
width | 260 | — | — |
collapsible | false | — | — |
defaultCollapsed | false | — | — |
labels | [] | — | — |
<lyra:app-sidebar
:groups="[
['heading' => 'Workspace', 'items' => [
['id' => 'overview', 'label' => 'Overview', 'active' => true],
['id' => 'projects', 'label' => 'Projects', 'badge' => '12'],
]],
['heading' => 'Account', 'items' => [
['id' => 'billing', 'label' => 'Billing'],
['id' => 'settings', 'label' => 'Settings'],
]],
]"
collapsible
/>