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.

Selected: Overview

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.

html
<!-- 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>. Pass aria-label through to it when the page has more than one navigation landmark.
  • In groups mode, href items are native links, asChild items preserve the router link, and action-only items remain buttons. The active item receives aria-current="page". The item callback runs before onSelect(id, item). Use target="_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 title and aria-label from 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

tsx
// 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

NameTypeRequiredDescription
brandReactNode—Optional brand content placed above the navigation groups.
groupsAppSidebarGroup[]—Convenience data mode. Each group is composed through {@link SidebarGroup}; destinations render as links, while items without destinations remain buttons.
footerReactNode—Optional utility links or user content separated below the navigation groups.
widthnumber—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.
collapsibleboolean—Whether to render a control that switches between expanded and icon-rail modes.
collapsedboolean—Controlled icon-rail state.
defaultCollapsedboolean—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.
labelsAppSidebarLabels—Localized labels for the collapse control.
childrenReactNode—Composition mode. Pass {@link SidebarGroup} children (including link children) to preserve their original element type and routing behavior.

x-data="lyraAppSidebar({ … })"

OptionTypeRequiredDescription
defaultCollapsedboolean—Whether the sidebar starts in its icon-rail state. Default: `false`.
widthnumber—Sidebar width in pixels while expanded. Default: `260`.
labelsLyraAppSidebarLabels—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:

html
<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.

PropDefaultRequiredExample values
brandnull——
groups[]——
footernull——
width260——
collapsiblefalse——
defaultCollapsedfalse——
labels[]——
blade
<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
/>