Frontile

Sidebar

Vertical navigation for an app: links, groups and folders in a full-height column that can collapse to an icon rail.

Import

import { Sidebar } from 'frontile';

Usage

Sidebar yields its parts. Put Item links in Body, give each an @href (or a @route) and its label as the block, and mark the current page with @isActive. The sidebar is as tall as its container.

import { Sidebar } from 'frontile';

<template>
  <div class='not-prose h-56'>
    <Sidebar as |sidebar|>
      <sidebar.Body>
        <sidebar.Item @href='#inbox' @isActive={{true}}>Inbox</sidebar.Item>
        <sidebar.Item @href='#drafts'>Drafts</sidebar.Item>
        <sidebar.Item @href='#sent'>Sent</sidebar.Item>
      </sidebar.Body>
    </Sidebar>
  </div>
</template>

Anatomy

  • Header, Body and Footer stack top to bottom; Body takes the free space and scrolls. Header and footer hold your own markup, such as a brand or an account menu.
  • Item is a link, or a button when it has neither @href nor @route.
  • Group puts a heading over a set of items.
  • Folder is a trigger that opens and closes a set of items.
  • Toggle collapses and expands the sidebar. There is no default toggle: place it where you want it.

Collapse the sidebar with its toggle.

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
import { Sidebar } from 'frontile';
import {
  ViewIcon,
  StarIcon,
  PackageIcon,
  SettingsIcon,
  LogoutIcon,
  SparklesIcon
} from 'site/components/icons';

export default class SidebarAnatomyExample extends Component {
  @tracked signedOut = false;

  signOut = () => {
    this.signedOut = true;
  };

  <template>
    <div class='demo-stack demo-stack--wide not-prose'>
      <div
        class='flex h-96 overflow-hidden rounded-lg border border-neutral-soft'
      >
        <Sidebar
          @label='Workspace'
          @class='border-e border-neutral-muted'
          as |sidebar|
        >
          <sidebar.Header>
            <SparklesIcon class='size-5 shrink-0 text-primary' />
            <span class='truncate text-sm font-semibold' data-sidebar-label>
              Acme
            </span>
            <sidebar.Toggle class='ml-auto' />
          </sidebar.Header>
          <sidebar.Body>
            <sidebar.Item
              @href='#overview'
              @icon={{ViewIcon}}
              @isActive={{true}}
            >
              Overview
            </sidebar.Item>
            <sidebar.Folder @label='Products' @icon={{PackageIcon}} as |folder|>
              <folder.Item @href='#all-products'>All products</folder.Item>
              <folder.Item @href='#categories'>Categories</folder.Item>
            </sidebar.Folder>
            <sidebar.Group @label='Saved'>
              <sidebar.Item @href='#starred' @icon={{StarIcon}}>
                Starred
              </sidebar.Item>
            </sidebar.Group>
          </sidebar.Body>
          <sidebar.Footer>
            <sidebar.Item @href='#settings' @icon={{SettingsIcon}}>
              Settings
            </sidebar.Item>
            <sidebar.Item @icon={{LogoutIcon}} {{on 'click' this.signOut}}>
              Sign out
            </sidebar.Item>
          </sidebar.Footer>
        </Sidebar>
        <p class='flex-1 p-4 text-sm text-neutral-firm'>
          {{if
            this.signedOut
            'Signed out.'
            'Collapse the sidebar with its toggle.'
          }}
        </p>
      </div>
    </div>
  </template>
}

Items

@icon takes an icon component; the label is the block. An item with @href is a plain anchor; with neither @href nor @route it is a <button> for your own handler, attached with the on modifier, like Sign out above.

  • @isActive marks the current page.
  • @isDisabled dims the item and drops its href, so it cannot be followed.
  • @isHidden renders nothing, for items the current user should not see.
import { Sidebar } from 'frontile';
import {
  ViewIcon,
  StarIcon,
  ArchiveIcon,
  DeleteIcon
} from 'site/components/icons';

<template>
  <div class='not-prose h-56'>
    <Sidebar @label='Projects' as |sidebar|>
      <sidebar.Body>
        <sidebar.Item @href='#all' @icon={{ViewIcon}} @isActive={{true}}>
          All projects
        </sidebar.Item>
        <sidebar.Item @href='#starred' @icon={{StarIcon}}>
          Starred
        </sidebar.Item>
        <sidebar.Item
          @href='#archived'
          @icon={{ArchiveIcon}}
          @isDisabled={{true}}
        >
          Archived
        </sidebar.Item>
        <sidebar.Item @href='#purge' @icon={{DeleteIcon}} @isHidden={{true}}>
          Purge data
        </sidebar.Item>
      </sidebar.Body>
    </Sidebar>
  </div>
</template>

An item's element is an <a>, a <button>, or an <li> row inside a collapsed folder's menu, so attributes you pass to it must suit all three.

Badges

@badge shows a count or short status at the end of the row. In the collapsed rail, where only the icon shows, it moves onto the icon's corner. 0, null, undefined and '' show nothing. Screen readers hear the badge after the label; @badgeLabel words it, such as "3 unread".

import { Sidebar } from 'frontile';
import { BookIcon, UserIcon } from 'site/components/icons';

<template>
  <div class='not-prose h-40'>
    <Sidebar @label='Inbox' as |sidebar|>
      <sidebar.Body>
        <sidebar.Item
          @href='#messages'
          @icon={{BookIcon}}
          @badge={{3}}
          @badgeLabel='3 unread'
        >
          Messages
        </sidebar.Item>
        <sidebar.Item @href='#requests' @badge='New'>
          Access requests
        </sidebar.Item>
        <sidebar.Item @href='#members' @icon={{UserIcon}}>
          Members
        </sidebar.Item>
      </sidebar.Body>
    </Sidebar>
  </div>
</template>

Linking to routes

With @route (plus @model, @models or @query as needed) an item renders a LinkTo, and the router decides whether it is active. A parent route counts as active on its child routes, so @route='users' stays active on users.show. @isActive always wins over the router; pass it when only an exact match should light up.

<sidebar.Item @route='dashboard' @icon={{ViewIcon}}>Dashboard</sidebar.Item>
<sidebar.Item @route='users.show' @model={{@currentUser.id}} @icon={{UserIcon}}>
  My profile
</sidebar.Item>

Groups

Group puts a heading over a set of items and names the set after it with @label.

import { Sidebar } from 'frontile';
import {
  ViewIcon,
  StarIcon,
  UserIcon,
  SettingsIcon
} from 'site/components/icons';

<template>
  <div class='not-prose h-72'>
    <Sidebar as |sidebar|>
      <sidebar.Body>
        <sidebar.Group @label='Projects'>
          <sidebar.Item @href='#all' @icon={{ViewIcon}} @isActive={{true}}>
            All projects
          </sidebar.Item>
          <sidebar.Item @href='#starred' @icon={{StarIcon}}>
            Starred
          </sidebar.Item>
        </sidebar.Group>
        <sidebar.Group @label='Admin'>
          <sidebar.Item @href='#members' @icon={{UserIcon}}>
            Members
          </sidebar.Item>
          <sidebar.Item @href='#settings' @icon={{SettingsIcon}}>
            Settings
          </sidebar.Item>
        </sidebar.Group>
      </sidebar.Body>
    </Sidebar>
  </div>
</template>

Folders

Folder opens and closes a set of items, one level deep. Give it a @label, an optional @icon, and write its items as folder.Item.

A folder holding the current page opens by itself, on load and whenever one of its items becomes the current page, but never reopens after the user closes it. @defaultOpen sets the starting state otherwise. To control it, pass @open and update it from @onOpenChange, which is also called for the automatic open.

The trigger is not styled active, since the item inside is the current page, but it carries data-active="true" for your own styles. A folder with no visible items is hidden, so a section the user may not see leaves no empty row.

import { Sidebar } from 'frontile';
import { PackageIcon, BookIcon } from 'site/components/icons';

<template>
  <div class='not-prose h-64'>
    <Sidebar @label='Catalog' as |sidebar|>
      <sidebar.Body>
        <sidebar.Folder @label='Products' @icon={{PackageIcon}} as |folder|>
          <folder.Item @href='#all-products' @isActive={{true}}>
            All products
          </folder.Item>
          <folder.Item @href='#categories'>Categories</folder.Item>
        </sidebar.Folder>
        <sidebar.Folder @label='Reports' @icon={{BookIcon}} as |folder|>
          <folder.Item @href='#sales'>Sales</folder.Item>
          <folder.Item @href='#inventory'>Inventory</folder.Item>
        </sidebar.Folder>
      </sidebar.Body>
    </Sidebar>
  </div>
</template>

Collapsing

Toggle collapses the sidebar to an icon rail and expands it again. In the rail:

  • Item labels and group headings are visually hidden but still name the links for screen readers, and each label shows as a tooltip on hover or focus.
  • A folder opens as a Dropdown menu beside the rail. Only put folder.Item in a folder's block: anything else renders twice in the rail.
  • Your own text in the header, body or footer stays visible unless you wrap it in <span data-sidebar-label>, which hides it the same way.

Collapse this sidebar, then hover the icons and open the folder.

Page content

import { Sidebar } from 'frontile';
import {
  ViewIcon,
  PackageIcon,
  StarIcon,
  SparklesIcon
} from 'site/components/icons';

<template>
  <div class='demo-stack demo-stack--wide not-prose'>
    <div
      class='flex h-80 overflow-hidden rounded-lg border border-neutral-soft'
    >
      <Sidebar
        @label='Workspace'
        @class='border-e border-neutral-muted'
        as |sidebar|
      >
        <sidebar.Header>
          <SparklesIcon class='size-5 shrink-0 text-primary' />
          <span class='truncate text-sm font-semibold' data-sidebar-label>
            Acme
          </span>
          <sidebar.Toggle class='ml-auto' />
        </sidebar.Header>
        <sidebar.Body>
          <sidebar.Item @href='#overview' @icon={{ViewIcon}} @isActive={{true}}>
            Overview
          </sidebar.Item>
          <sidebar.Folder @label='Products' @icon={{PackageIcon}} as |folder|>
            <folder.Item @href='#all-products'>All products</folder.Item>
            <folder.Item @href='#categories'>Categories</folder.Item>
          </sidebar.Folder>
          <sidebar.Group @label='Saved'>
            <sidebar.Item @href='#starred' @icon={{StarIcon}}>
              Starred
            </sidebar.Item>
          </sidebar.Group>
        </sidebar.Body>
      </Sidebar>
      <p class='flex-1 p-4 text-sm text-neutral-firm'>Page content</p>
    </div>
  </div>
</template>

Toggle placement

Toggle works anywhere inside the sidebar and stays visible in the rail. In the header, put class='ml-auto' on it to push it to the end; in the footer it lines up with the items by itself.

import { Sidebar } from 'frontile';
import { ViewIcon, SettingsIcon } from 'site/components/icons';

<template>
  <div class='not-prose h-56'>
    <Sidebar @label='Footer toggle' as |sidebar|>
      <sidebar.Body>
        <sidebar.Item @href='#overview' @icon={{ViewIcon}} @isActive={{true}}>
          Overview
        </sidebar.Item>
        <sidebar.Item @href='#settings' @icon={{SettingsIcon}}>
          Settings
        </sidebar.Item>
      </sidebar.Body>
      <sidebar.Footer>
        <sidebar.Toggle />
      </sidebar.Footer>
    </Sidebar>
  </div>
</template>

Collapse modes

@collapsible sets what collapsing does: icon (the default) shrinks the sidebar to a rail, offcanvas slides it closed, and none keeps it expanded.

A closed offcanvas sidebar cannot hold the button that brings it back, so these demos use a toggle outside the sidebar.

@collapsible='icon'
@collapsible='offcanvas'
@collapsible='none'
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Sidebar, SidebarToggle } from 'frontile';
import { ViewIcon, BookIcon } from 'site/components/icons';

class Mode {
  @tracked collapsed = false;
  name: string;
  id: string;
  constructor(name: string) {
    this.name = name;
    this.id = `collapse-mode-${name}`;
  }
  toggle = () => (this.collapsed = !this.collapsed);
}

export default class CollapseModesExample extends Component {
  modes = ['icon', 'offcanvas', 'none'].map((name) => new Mode(name));

  <template>
    <div class='demo-stack demo-stack--wide not-prose'>
      {{#each this.modes as |mode|}}
        <div
          class='flex h-40 overflow-hidden rounded-lg border border-neutral-soft'
        >
          <Sidebar
            @collapsed={{mode.collapsed}}
            @collapsible={{mode.name}}
            @label={{mode.name}}
            @class='border-e border-neutral-muted'
            id={{mode.id}}
            as |sidebar|
          >
            <sidebar.Body>
              <sidebar.Item @href='#home' @icon={{ViewIcon}}>Home</sidebar.Item>
              <sidebar.Item @href='#docs' @icon={{BookIcon}}>Docs</sidebar.Item>
            </sidebar.Body>
          </Sidebar>
          <div class='flex flex-1 items-start gap-2 p-2 text-sm'>
            <SidebarToggle
              @collapsed={{mode.collapsed}}
              @onToggle={{mode.toggle}}
              @controls={{mode.id}}
            />
            <code class='py-2'>@collapsible='{{mode.name}}'</code>
          </div>
        </div>
      {{/each}}
    </div>
  </template>
}

A toggle outside the sidebar

On its own the sidebar keeps its collapsed state itself, starting from @defaultCollapsed. To drive it from outside, pass @collapsed and change it yourself: from a SidebarToggle anywhere on the page, which calls @onToggle, or from @onCollapsedChange, which the sidebar's own Toggle calls with the value it asks for. Give the sidebar an id and pass it to the toggle as @controls, so its aria-controls points at the sidebar. The Collapse modes demo above is wired this way.

<Sidebar @collapsed={{this.collapsed}} id='main-nav' as |sidebar|>
  ...
</Sidebar>

<SidebarToggle
  @collapsed={{this.collapsed}}
  @onToggle={{this.toggle}}
  @controls='main-nav'
/>

In an AppLayout the layout holds this state; put layout.SidebarToggle where you want it, such as the navbar.

Using the state

The sidebar also yields its state, isCollapsed and isInDrawer, and the actions toggle, collapse and expand, so the block can render differently in each state. isCollapsed is true in the rail and while an offcanvas sidebar is closed; isInDrawer is true when an AppLayout shows the sidebar in its phone drawer. Collapse this one to swap the trial card for a single item.

Page content

import { Button, Sidebar } from 'frontile';
import { on } from '@ember/modifier';
import { ViewIcon, RocketIcon, SparklesIcon } from 'site/components/icons';

<template>
  <div class='demo-stack demo-stack--wide not-prose'>
    <div
      class='flex h-72 overflow-hidden rounded-lg border border-neutral-soft'
    >
      <Sidebar
        @label='Workspace'
        @class='border-e border-neutral-muted'
        as |sidebar|
      >
        <sidebar.Header>
          <SparklesIcon class='size-5 shrink-0 text-primary' />
          <span class='truncate text-sm font-semibold' data-sidebar-label>
            Acme
          </span>
          <sidebar.Toggle class='ml-auto' />
        </sidebar.Header>
        <sidebar.Body>
          <sidebar.Item @href='#overview' @icon={{ViewIcon}} @isActive={{true}}>
            Overview
          </sidebar.Item>
        </sidebar.Body>
        <sidebar.Footer>
          {{#if sidebar.isCollapsed}}
            <sidebar.Item @icon={{RocketIcon}} {{on 'click' sidebar.expand}}>
              Upgrade
            </sidebar.Item>
          {{else}}
            <div class='rounded-lg border border-neutral-muted p-3 text-sm'>
              <p class='font-medium'>Trial ends in 5 days</p>
              <Button @size='xs' @color='primary' @class='mt-2'>
                Upgrade
              </Button>
            </div>
          {{/if}}
        </sidebar.Footer>
      </Sidebar>
      <p class='flex-1 p-4 text-sm text-neutral-firm'>Page content</p>
    </div>
  </div>
</template>

Item layouts and the rail

@layout sets how an item arranges its icon and label:

  • inline (default): the label beside the icon.
  • stacked: a small label under the icon. @shortLabel replaces the visible text; the full label still names the link.
  • icon: the icon alone, with the label as a tooltip.

stacked and icon ignore collapsing and suit @size='rail', a narrow column meant to stay put, so pair it with @collapsible='none'. A rail centers its header over the items.

import { Sidebar } from 'frontile';
import {
  ViewIcon,
  SettingsIcon,
  UserIcon,
  SparklesIcon
} from 'site/components/icons';

<template>
  <div class='not-prose flex h-64 flex-wrap items-start gap-8'>
    <Sidebar @collapsible='none' @label='Inline' as |sidebar|>
      <sidebar.Body>
        <sidebar.Item @href='#overview' @icon={{ViewIcon}} @isActive={{true}}>
          Overview
        </sidebar.Item>
        <sidebar.Item @href='#members' @icon={{UserIcon}}>
          Members
        </sidebar.Item>
      </sidebar.Body>
    </Sidebar>
    <Sidebar @collapsible='none' @size='rail' @label='Stacked' as |sidebar|>
      <sidebar.Header>
        <SparklesIcon class='size-5 shrink-0 text-primary' />
      </sidebar.Header>
      <sidebar.Body>
        <sidebar.Item
          @href='#overview'
          @layout='stacked'
          @icon={{ViewIcon}}
          @isActive={{true}}
        >
          Overview
        </sidebar.Item>
        <sidebar.Item
          @href='#settings'
          @layout='stacked'
          @icon={{SettingsIcon}}
          @shortLabel='Prefs'
        >
          Settings
        </sidebar.Item>
      </sidebar.Body>
    </Sidebar>
    <Sidebar @collapsible='none' @size='rail' @label='Icon only' as |sidebar|>
      <sidebar.Body>
        <sidebar.Item
          @href='#overview'
          @layout='icon'
          @icon={{ViewIcon}}
          @isActive={{true}}
        >
          Overview
        </sidebar.Item>
        <sidebar.Item @href='#members' @layout='icon' @icon={{UserIcon}}>
          Members
        </sidebar.Item>
      </sidebar.Body>
    </Sidebar>
  </div>
</template>

Styling

On its own the sidebar has no background or border: it sits on its container's surface. Draw an edge with @class, as the demos above do with @class='border-e border-neutral-muted'. In an AppLayout the layout draws both.

The header, body and footer share one content line, so items, group headings and your own markup line up without padding of your own. To keep a brand in place when the sidebar collapses, give its icon size-5, as the demos do.

@class on the sidebar or on an item is merged with the theme's classes, so a conflicting class wins. Use it on the sidebar for another width, such as @class='w-48'. That width holds in the rail too, so pair it with @collapsible='none'.

import { Sidebar } from 'frontile';
import { StarIcon, ViewIcon } from 'site/components/icons';

<template>
  <div class='not-prose h-32'>
    <Sidebar @label='Favorites' @class='w-48' @collapsible='none' as |sidebar|>
      <sidebar.Body>
        <sidebar.Item @href='#all' @icon={{ViewIcon}}>All</sidebar.Item>
        <sidebar.Item @href='#starred' @icon={{StarIcon}} @class='text-primary'>
          Starred
        </sidebar.Item>
      </sidebar.Body>
    </Sidebar>
  </div>
</template>

In an AppLayout

Inside an AppLayout, use the yielded layout.Sidebar. It shares the layout's state, takes the first column (or the last, with @sidebarSide='end'), and on small screens moves into a drawer or gives way to a BottomNav. Render one Sidebar per layout. The AppLayout page shows each of these.

Accessibility

  • The sidebar is a nav landmark named by @label (default Main). Give each navigation on the page its own name.
  • The active item has aria-current="page". A disabled item has aria-disabled="true" and no href; a disabled button is natively disabled.
  • An item's @icon and @shortLabel are hidden from assistive technology, so the full label names the link in every layout and in the rail.
  • A @badge is read after the label; give @badgeLabel when the bare value is unclear.
  • Group is a group named by its @label.
  • Toggle is a button named by @label (default "Toggle sidebar"), with aria-expanded and aria-controls pointing at the sidebar.
  • A folder's trigger is a button named by @label. Inline it is a disclosure (aria-expanded, aria-controls) over a group of its items. In the rail it is a menu button, and its items are link items of a menu, so they still open in a new tab. A closed folder keeps its items out of the tab order.

Folder keyboard

Inline:

Key Action
Enter or Space On the trigger, opens or closes the folder
Tab Moves through the trigger and, while open, its items

In the icon rail:

Key Action
Enter, Space, ArrowDown or ArrowUp On the trigger, opens the menu
Letters Opens the menu, or moves to the next item whose label starts with them
ArrowDown or ArrowUp In the menu, moves between items, skipping disabled ones
Enter Follows the item's link and closes the menu
Escape or Tab Closes the menu and returns focus to the trigger

API

Sidebar

Element: HTMLElement

The sidebar navigation: a full-height nav landmark that can collapse to an icon rail. Inside AppLayout it takes the first (or, with @sidebarSide="end", the last) column. Below the layout's @breakpoint the same block renders inside a Drawer instead, opened by MenuToggle, so the sidebar is written once. It also works on its own, always inline, with @collapsed or its own state.

Arguments

Name Type Default Description
class string - Classes for the sidebar, merged with the theme's so a conflicting class wins: @class="w-72" replaces the width. A width given this way applies whether or not the sidebar is collapsed, so pair it with @collapsible="none". Applies to the inline sidebar, not the drawer.
collapsed boolean - Whether the sidebar is collapsed, for a sidebar you control. Toggle and the yielded actions then call @onCollapsedChange and leave the change to you. Without it the sidebar keeps its own state, starting from @defaultCollapsed. Only used outside an AppLayout; inside one, use the layout's @sidebarCollapsed.
collapsible enum 'icon' How the sidebar collapses. icon shrinks it to a rail where item labels are visually hidden but stay available to assistive technology; offcanvas hides it entirely while collapsed; none ignores collapse.
defaultCollapsed boolean false Whether the sidebar starts collapsed when it keeps its own state (no @collapsed). Only used outside an AppLayout.
label string 'Main' The accessible name of the sidebar's nav landmark. Give each navigation on the page a distinct name.
layout Object - Supplied by AppLayout. Not part of the public API.
onCollapsedChange function - Called with the requested collapsed state when Toggle or a yielded action changes it. Only used outside an AppLayout.
size enum 'default' The sidebar's width preset. default is the standard sidebar; rail is a narrow column (80px) for a stack of icon-over-label items, usually with @collapsible="none". It is a named preset, not a length: for any other width, pass a width utility as @class (and set --app-sidebar-width yourself if you read the token).
variant enum 'attached' The layout variant, inset or attached, which sets the sidebar's widths and the gutter around its rows. Only used when the sidebar is rendered outside an AppLayout; inside one it follows the layout.

Blocks

Name Type Default Description
default * Array - Yields Header, Body and Footer sections, Item links, Folder for a collapsible set of items, Group to label a set of items, and Toggle, the button that collapses and expands the sidebar. It also yields the sidebar's state, isCollapsed and isInDrawer, and the actions toggle, collapse and expand. Place Toggle yourself, usually in Header or Footer; there is no default toggle. In the phone drawer Toggle renders nothing (the drawer is always expanded and has its own close button), so the same markup works at every width. In the icon rail, Item labels are visually hidden but still read by screen readers; for your own markup, wrap the text in <span data-sidebar-label> to get the same behavior.

Item

NavItem

Element: enum

A styled navigation link. It takes the same routing arguments as TabNav.Item: @route renders a LinkTo and the router decides whether it is active; @href renders a plain anchor and needs no router. The active item carries aria-current="page". In the icon rail the label is visually hidden but keeps naming the link, and shows as a tooltip on hover or focus.

Arguments

Name Type Default Description
badge enum - A count or short status shown as a pill on the icon. Nothing is shown for undefined, null, 0 or an empty string.
badgeDot boolean - Supplied by BottomNav.More: shows the badge as a small dot with no text, whatever @badge is. @badgeLabel is its screen-reader text. Not part of the public API.
badgeLabel string - Screen-reader text for the badge, read after the label. Defaults to the badge value.
class string - Class names added to this item's theme classes. Merged with Tailwind Merge, so a class that conflicts with a theme class wins.
folder Object - Supplied by NavFolder: the folder this item belongs to. The item registers itself with it so the folder can follow its active and hidden state. Not part of the public API.
href string - Renders a plain anchor. Ignored when @route is given.
icon ComponentLike - An icon component, shown before the label. In the icon rail it is the only visible part of the item. It is hidden from assistive technology.
isActive boolean - Overrides the active state. Always wins over anything derived from the router, and is the only source of truth when @route is not used.
isDisabled boolean false Marks the link as disabled. The href is dropped as well, so the link cannot be followed; an item with neither @href nor @route is a button, which gets the native disabled attribute.
isHidden boolean false Renders nothing at all. For items the current user may not see.
layout enum 'inline' How the item lays out its icon and label. inline puts the label beside the icon and follows the sidebar's icon rail; stacked puts a small label under the icon; icon shows the icon alone, with the label as a tooltip. stacked and icon ignore the sidebar's collapsed state.
menuItem ListboxItem (manager, onLinkActivate bound) - Supplied by NavFolder in the icon rail: the Dropdown menu's item component. The item then renders as a link row of that menu (a role="menuitem" holding the link) instead of a nav row. Not part of the public API.
model unknown - A single dynamic segment for @route.
models Array - Dynamic segments for @route.
noTooltip boolean - Supplied by BottomNav.More: never shows the label as a tooltip, even in the icon layout. Not part of the public API.
onActivate function - Supplied by Sidebar while it renders in the mobile drawer: called when the item's link is clicked, so the drawer closes. Not called for a disabled item. Not part of the public API.
query Record<string, unknown> - Query params for @route.
rail boolean - Supplied by Sidebar: whether the sidebar is drawn as the icon rail. Not part of the public API.
registerOnly boolean - Supplied by NavFolder in the icon rail: the item only registers with its folder and renders nothing, so the folder can follow its active state while its menu is closed. Not part of the public API.
route string - Renders a LinkTo for this route and derives the active state from the router. Omit it (and pass @href) to stay entirely router-free.
shortLabel string - Shorter text shown in place of the label in the stacked layout. The full label stays the link's accessible name.
tooltipPlacement enum - Supplied by Sidebar (or BottomNav): the side of the item its tooltip opens on. Not part of the public API.

Blocks

Name Type Default Description
default * Array - The item's label.

Folder

NavFolder

Element: HTMLDivElement

A collapsible group of navigation items, one level deep. The trigger is a disclosure button (aria-expanded, aria-controls); the items sit in a role="group" panel labelled by it. The folder opens when one of its items is active, on load and whenever an item newly becomes active, but never forces itself open otherwise. It is hidden when none of its items is visible.

In the collapsed icon rail (not in the mobile drawer) the trigger is a Dropdown menu button instead, and the items are the menu's link items, beside the rail. The menu brings the keyboard model (arrows, type-ahead, Escape returning focus to the trigger); tabbing out of it or following an item closes it.

Arguments

Name Type Default Description
label * string - The folder's label, shown in its trigger.
class string - Class names added to the folder's root element. Merged with Tailwind Merge, so a class that conflicts with a theme class wins.
defaultOpen boolean false The initial open state when @open is not given.
icon ComponentLike - An icon component, shown before the label. It is hidden from assistive technology.
isHidden boolean false Hides the folder. A folder is also hidden when none of its items is visible.
onActivate function - Supplied by Sidebar while it renders in the mobile drawer: handed to each item so following one closes the drawer. Not part of the public API.
onOpenChange function - Called with the requested inline open state whenever it would change. Not called when the menu of the collapsed icon rail opens or closes.
open boolean -

Makes the open state controlled. Clicks then only call @onOpenChange, and so does an automatic open when a child becomes active.

This is the inline open state. In the collapsed icon rail the folder opens as a menu instead, whose state is its own: @open does not control it and @onOpenChange is not called for it. The inline state still follows an active child there, and shows on expanding.

parent Object - Supplied by BottomNav.More: the container this folder belongs to. The folder registers a handle with it that reports its children: active when a visible child is, hidden when the folder or every child is, badged when a visible child is. Not part of the public API.
rail boolean - Supplied by Sidebar: whether the sidebar is drawn as the icon rail, where the folder opens as a menu. Not part of the public API.
tooltipPlacement enum 'right' Supplied by Sidebar: the side rail tooltips, and so the menu, open on. Not part of the public API.

Blocks

Name Type Default Description
default * Array - Yields Item, a NavItem that belongs to this folder.

Group

SidebarGroup

Element: HTMLDivElement

A labelled set of sidebar items: a group named by its visible @label.

Arguments

Name Type Default Description
label * string - The group's heading. It is shown above the items and names the group for assistive technology. In the icon rail it is visually hidden.
class string - Class names appended to this group's theme classes.
classes Object - Supplied by Sidebar. Not part of the public API. Without it (a group rendered on its own) the standalone sidebar's classes are used.

Blocks

Name Type Default Description
default * Array - The group's items.

SidebarToggle

SidebarToggle

Element: HTMLButtonElement

A button that collapses and expands the sidebar. aria-expanded is true while the sidebar is expanded. Inside an AppLayout, below its @breakpoint (where the sidebar is a drawer) it opens the drawer instead, like MenuToggle, and aria-expanded follows the drawer. With @mobileNav="bottom" there is no sidebar below the breakpoint, so there it renders nothing.

Arguments

Name Type Default Description
collapsed boolean - Whether the sidebar it controls is collapsed, for aria-expanded. Only used outside an AppLayout; inside one the button follows the layout.
controls string - The id of the sidebar this button controls, for aria-controls. Inside an AppLayout it defaults to the layout's sidebar, which below the breakpoint is the nav inside the drawer.
label string 'Toggle sidebar' The accessible name of the button.
layout Object - Supplied by AppLayout. Not part of the public API.
onToggle function - Called when the button is pressed. Only used outside an AppLayout, where you flip the value you pass to the Sidebar's @collapsed; inside one the button changes the layout's state itself.

Blocks

Name Type Default Description
default * Array - Replaces the default icon with your own content.
Released under MIT License - Created by Josemar Luedke