Sidebar

A collapsible app sidebar with a mobile drawer and saved state.

import { โ€ฆ } from '@inlaylab/ivorie/sidebar'
Open issues
24
In review
7
Shipped
132
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LucideLayoutDashboard, LucideInbox, LucideFolderKanban, LucideCalendar, LucideUsers, LucidePlus, LucideEllipsis, LucideChevronsUpDown, LucideSparkles } from '@lucide/angular';
import { IV_SIDEBAR_IMPORTS } from '@inlaylab/ivorie/sidebar';

// A full app shell. The trigger or the rail hides the sidebar off-canvas; Ctrl+B also works.
// cookieName="" so this demo never saves its state over the site's own sidebar.
@Component({
  selector: 'demo-sidebar',
  imports: [IV_SIDEBAR_IMPORTS, LucideLayoutDashboard, LucideInbox, LucideFolderKanban, LucideCalendar, LucideUsers, LucidePlus, LucideEllipsis, LucideChevronsUpDown, LucideSparkles],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="relative h-[560px] w-full transform-gpu overflow-hidden rounded-xl border">
      <iv-sidebar-provider cookieName="" class="h-full min-h-0">
        <iv-sidebar collapsible="offcanvas" class="h-full">
          <iv-sidebar-header>
            <ul ivSidebarMenu>
              <li ivSidebarMenuItem>
                <iv-sidebar-menu-button size="lg" tooltip="Acme Inc">
                  <span class="bg-primary text-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg">
                    <svg lucideSparkles></svg>
                  </span>
                  <span class="grid flex-1 text-left leading-tight">
                    <span class="truncate font-medium">Acme Inc</span>
                    <span class="text-muted-foreground truncate text-xs">Pro plan</span>
                  </span>
                  <svg lucideChevronsUpDown class="text-muted-foreground ml-auto"></svg>
                </iv-sidebar-menu-button>
              </li>
            </ul>
          </iv-sidebar-header>
          <iv-sidebar-content>
            <iv-sidebar-group>
              <iv-sidebar-group-label>Workspace</iv-sidebar-group-label>
              <ul ivSidebarMenu>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button [isActive]="true" tooltip="Dashboard">
                    <svg lucideLayoutDashboard></svg><span>Dashboard</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Inbox">
                    <svg lucideInbox></svg><span>Inbox</span>
                  </iv-sidebar-menu-button>
                  <iv-sidebar-menu-badge>12</iv-sidebar-menu-badge>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Projects">
                    <svg lucideFolderKanban></svg><span>Projects</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Calendar">
                    <svg lucideCalendar></svg><span>Calendar</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Team">
                    <svg lucideUsers></svg><span>Team</span>
                  </iv-sidebar-menu-button>
                </li>
              </ul>
            </iv-sidebar-group>
            <iv-sidebar-group>
              <iv-sidebar-group-label>Projects</iv-sidebar-group-label>
              <iv-sidebar-group-action title="Add project">
                <svg lucidePlus></svg><span class="sr-only">Add project</span>
              </iv-sidebar-group-action>
              <ul ivSidebarMenu>
                @for (p of projects; track p) {
                  <li ivSidebarMenuItem>
                    <iv-sidebar-menu-button [tooltip]="p">
                      <span class="bg-muted-foreground/40 size-2 shrink-0 rounded-full"></span><span>{{ p }}</span>
                    </iv-sidebar-menu-button>
                    <iv-sidebar-menu-action [showOnHover]="true" title="More">
                      <svg lucideEllipsis></svg><span class="sr-only">More for {{ p }}</span>
                    </iv-sidebar-menu-action>
                  </li>
                }
              </ul>
            </iv-sidebar-group>
          </iv-sidebar-content>
          <iv-sidebar-footer>
            <ul ivSidebarMenu>
              <li ivSidebarMenuItem>
                <iv-sidebar-menu-button size="lg" tooltip="Maya Ross">
                  <span class="bg-muted flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-medium">MR</span>
                  <span class="grid flex-1 text-left leading-tight">
                    <span class="truncate font-medium">Maya Ross</span>
                    <span class="text-muted-foreground truncate text-xs">maya&#64;acme.dev</span>
                  </span>
                  <svg lucideChevronsUpDown class="text-muted-foreground ml-auto"></svg>
                </iv-sidebar-menu-button>
              </li>
            </ul>
          </iv-sidebar-footer>
          <iv-sidebar-rail />
        </iv-sidebar>
        <main ivSidebarInset class="min-w-0">
          <header class="flex h-12 shrink-0 items-center gap-2 border-b px-3">
            <iv-sidebar-trigger />
            <span class="bg-border h-4 w-px shrink-0"></span>
            <nav aria-label="Breadcrumb" class="flex min-w-0 items-center gap-1.5 text-sm">
              <span class="text-muted-foreground hidden sm:inline">Workspace</span>
              <span class="text-muted-foreground hidden sm:inline">/</span>
              <span class="truncate font-medium">Dashboard</span>
            </nav>
          </header>
          <div class="flex min-h-0 flex-1 flex-col gap-3 p-4">
            <div class="grid grid-cols-3 gap-3">
              @for (s of stats; track s.label) {
                <div class="bg-muted/50 rounded-xl p-3">
                  <div class="text-muted-foreground truncate text-xs">{{ s.label }}</div>
                  <div class="mt-1 text-lg font-semibold tabular-nums">{{ s.value }}</div>
                </div>
              }
            </div>
            <div class="bg-muted/50 flex-1 rounded-xl"></div>
          </div>
        </main>
      </iv-sidebar-provider>
    </div>
  `,
})
export default class SidebarDemo {
  protected readonly projects = ['Website redesign', 'Mobile app', 'Brand refresh'];
  protected readonly stats = [
    { label: 'Open issues', value: '24' },
    { label: 'In review', value: '7' },
    { label: 'Shipped', value: '132' },
  ];
}

Installation

CLInpm
npx ivorie add sidebar

Ports the shadcn/ui Sidebar. Desktop modes: slide off-screen, collapse to icons, or fixed. Below 768px it is a modal drawer. The open state is saved in a cookie and works with SSR.

Usage

import { IV_SIDEBAR_IMPORTS } from '@inlaylab/ivorie/sidebar';
<iv-sidebar-provider>
  <iv-sidebar>
    <iv-sidebar-content>
      <iv-sidebar-group>
        <iv-sidebar-group-label>Navigation</iv-sidebar-group-label>
        <iv-sidebar-group-content>
          <ul ivSidebarMenu>
            <li ivSidebarMenuItem>
              <iv-sidebar-menu-button [isActive]="true" tooltip="Dashboard">
                <span>Dashboard</span>
              </iv-sidebar-menu-button>
            </li>
          </ul>
        </iv-sidebar-group-content>
      </iv-sidebar-group>
    </iv-sidebar-content>
  </iv-sidebar>

  <main ivSidebarInset>
    <iv-sidebar-trigger />
    <!-- page content -->
  </main>
</iv-sidebar-provider>

Examples

Collapse to icons

Open issues
24
In review
7
Shipped
132
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LucideLayoutDashboard, LucideInbox, LucideFolderKanban, LucideCalendar, LucideUsers, LucidePlus, LucideEllipsis, LucideChevronsUpDown, LucideSparkles } from '@lucide/angular';
import { IV_SIDEBAR_IMPORTS } from '@inlaylab/ivorie/sidebar';

// Collapsed, only icons show; hover one to see its label as a tooltip.
// cookieName="" so this demo never saves its state over the site's own sidebar.
@Component({
  selector: 'demo-sidebar-collapse-to-icons',
  imports: [IV_SIDEBAR_IMPORTS, LucideLayoutDashboard, LucideInbox, LucideFolderKanban, LucideCalendar, LucideUsers, LucidePlus, LucideEllipsis, LucideChevronsUpDown, LucideSparkles],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="relative h-[560px] w-full transform-gpu overflow-hidden rounded-xl border">
      <iv-sidebar-provider cookieName="" [defaultOpen]="false" class="h-full min-h-0">
        <iv-sidebar collapsible="icon" class="h-full">
          <iv-sidebar-header>
            <ul ivSidebarMenu>
              <li ivSidebarMenuItem>
                <iv-sidebar-menu-button size="lg" tooltip="Acme Inc">
                  <span class="bg-primary text-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg">
                    <svg lucideSparkles></svg>
                  </span>
                  <span class="grid flex-1 text-left leading-tight">
                    <span class="truncate font-medium">Acme Inc</span>
                    <span class="text-muted-foreground truncate text-xs">Pro plan</span>
                  </span>
                  <svg lucideChevronsUpDown class="text-muted-foreground ml-auto"></svg>
                </iv-sidebar-menu-button>
              </li>
            </ul>
          </iv-sidebar-header>
          <iv-sidebar-content>
            <iv-sidebar-group>
              <iv-sidebar-group-label>Workspace</iv-sidebar-group-label>
              <ul ivSidebarMenu>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button [isActive]="true" tooltip="Dashboard">
                    <svg lucideLayoutDashboard></svg><span>Dashboard</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Inbox">
                    <svg lucideInbox></svg><span>Inbox</span>
                  </iv-sidebar-menu-button>
                  <iv-sidebar-menu-badge>12</iv-sidebar-menu-badge>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Projects">
                    <svg lucideFolderKanban></svg><span>Projects</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Calendar">
                    <svg lucideCalendar></svg><span>Calendar</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Team">
                    <svg lucideUsers></svg><span>Team</span>
                  </iv-sidebar-menu-button>
                </li>
              </ul>
            </iv-sidebar-group>
            <iv-sidebar-group>
              <iv-sidebar-group-label>Projects</iv-sidebar-group-label>
              <iv-sidebar-group-action title="Add project">
                <svg lucidePlus></svg><span class="sr-only">Add project</span>
              </iv-sidebar-group-action>
              <ul ivSidebarMenu>
                @for (p of projects; track p) {
                  <li ivSidebarMenuItem>
                    <iv-sidebar-menu-button [tooltip]="p">
                      <span class="bg-muted-foreground/40 size-2 shrink-0 rounded-full"></span><span>{{ p }}</span>
                    </iv-sidebar-menu-button>
                    <iv-sidebar-menu-action [showOnHover]="true" title="More">
                      <svg lucideEllipsis></svg><span class="sr-only">More for {{ p }}</span>
                    </iv-sidebar-menu-action>
                  </li>
                }
              </ul>
            </iv-sidebar-group>
          </iv-sidebar-content>
          <iv-sidebar-footer>
            <ul ivSidebarMenu>
              <li ivSidebarMenuItem>
                <iv-sidebar-menu-button size="lg" tooltip="Maya Ross">
                  <span class="bg-muted flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-medium">MR</span>
                  <span class="grid flex-1 text-left leading-tight">
                    <span class="truncate font-medium">Maya Ross</span>
                    <span class="text-muted-foreground truncate text-xs">maya&#64;acme.dev</span>
                  </span>
                  <svg lucideChevronsUpDown class="text-muted-foreground ml-auto"></svg>
                </iv-sidebar-menu-button>
              </li>
            </ul>
          </iv-sidebar-footer>
          <iv-sidebar-rail />
        </iv-sidebar>
        <main ivSidebarInset class="min-w-0">
          <header class="flex h-12 shrink-0 items-center gap-2 border-b px-3">
            <iv-sidebar-trigger />
            <span class="bg-border h-4 w-px shrink-0"></span>
            <nav aria-label="Breadcrumb" class="flex min-w-0 items-center gap-1.5 text-sm">
              <span class="text-muted-foreground hidden sm:inline">Workspace</span>
              <span class="text-muted-foreground hidden sm:inline">/</span>
              <span class="truncate font-medium">Dashboard</span>
            </nav>
          </header>
          <div class="flex min-h-0 flex-1 flex-col gap-3 p-4">
            <div class="grid grid-cols-3 gap-3">
              @for (s of stats; track s.label) {
                <div class="bg-muted/50 rounded-xl p-3">
                  <div class="text-muted-foreground truncate text-xs">{{ s.label }}</div>
                  <div class="mt-1 text-lg font-semibold tabular-nums">{{ s.value }}</div>
                </div>
              }
            </div>
            <div class="bg-muted/50 flex-1 rounded-xl"></div>
          </div>
        </main>
      </iv-sidebar-provider>
    </div>
  `,
})
export default class SidebarCollapseToIconsDemo {
  protected readonly projects = ['Website redesign', 'Mobile app', 'Brand refresh'];
  protected readonly stats = [
    { label: 'Open issues', value: '24' },
    { label: 'In review', value: '7' },
    { label: 'Shipped', value: '132' },
  ];
}

Floating and inset

Dashboard
Open issues
24
In review
7
Shipped
132
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { LucideLayoutDashboard, LucideInbox, LucideFolderKanban, LucideCalendar, LucideUsers, LucidePlus, LucideEllipsis, LucideChevronsUpDown, LucideSparkles } from '@lucide/angular';
import { IvButton } from '@inlaylab/ivorie/button';
import { IV_SIDEBAR_IMPORTS } from '@inlaylab/ivorie/sidebar';

// Floating draws the sidebar as a card; inset also sets the page in a card. Switch between them in the header.
// cookieName="" so this demo never saves its state over the site's own sidebar.
@Component({
  selector: 'demo-sidebar-floating-and-inset',
  imports: [IV_SIDEBAR_IMPORTS, IvButton, LucideLayoutDashboard, LucideInbox, LucideFolderKanban, LucideCalendar, LucideUsers, LucidePlus, LucideEllipsis, LucideChevronsUpDown, LucideSparkles],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="relative h-[560px] w-full transform-gpu overflow-hidden rounded-xl border">
      <iv-sidebar-provider cookieName="" class="h-full min-h-0">
        <iv-sidebar [variant]="variant()" collapsible="icon" class="h-full">
          <iv-sidebar-header>
            <ul ivSidebarMenu>
              <li ivSidebarMenuItem>
                <iv-sidebar-menu-button size="lg" tooltip="Acme Inc">
                  <span class="bg-primary text-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg">
                    <svg lucideSparkles></svg>
                  </span>
                  <span class="grid flex-1 text-left leading-tight">
                    <span class="truncate font-medium">Acme Inc</span>
                    <span class="text-muted-foreground truncate text-xs">Pro plan</span>
                  </span>
                  <svg lucideChevronsUpDown class="text-muted-foreground ml-auto"></svg>
                </iv-sidebar-menu-button>
              </li>
            </ul>
          </iv-sidebar-header>
          <iv-sidebar-content>
            <iv-sidebar-group>
              <iv-sidebar-group-label>Workspace</iv-sidebar-group-label>
              <ul ivSidebarMenu>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button [isActive]="true" tooltip="Dashboard">
                    <svg lucideLayoutDashboard></svg><span>Dashboard</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Inbox">
                    <svg lucideInbox></svg><span>Inbox</span>
                  </iv-sidebar-menu-button>
                  <iv-sidebar-menu-badge>12</iv-sidebar-menu-badge>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Projects">
                    <svg lucideFolderKanban></svg><span>Projects</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Calendar">
                    <svg lucideCalendar></svg><span>Calendar</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Team">
                    <svg lucideUsers></svg><span>Team</span>
                  </iv-sidebar-menu-button>
                </li>
              </ul>
            </iv-sidebar-group>
            <iv-sidebar-group>
              <iv-sidebar-group-label>Projects</iv-sidebar-group-label>
              <iv-sidebar-group-action title="Add project">
                <svg lucidePlus></svg><span class="sr-only">Add project</span>
              </iv-sidebar-group-action>
              <ul ivSidebarMenu>
                @for (p of projects; track p) {
                  <li ivSidebarMenuItem>
                    <iv-sidebar-menu-button [tooltip]="p">
                      <span class="bg-muted-foreground/40 size-2 shrink-0 rounded-full"></span><span>{{ p }}</span>
                    </iv-sidebar-menu-button>
                    <iv-sidebar-menu-action [showOnHover]="true" title="More">
                      <svg lucideEllipsis></svg><span class="sr-only">More for {{ p }}</span>
                    </iv-sidebar-menu-action>
                  </li>
                }
              </ul>
            </iv-sidebar-group>
          </iv-sidebar-content>
          <iv-sidebar-footer>
            <ul ivSidebarMenu>
              <li ivSidebarMenuItem>
                <iv-sidebar-menu-button size="lg" tooltip="Maya Ross">
                  <span class="bg-muted flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-medium">MR</span>
                  <span class="grid flex-1 text-left leading-tight">
                    <span class="truncate font-medium">Maya Ross</span>
                    <span class="text-muted-foreground truncate text-xs">maya&#64;acme.dev</span>
                  </span>
                  <svg lucideChevronsUpDown class="text-muted-foreground ml-auto"></svg>
                </iv-sidebar-menu-button>
              </li>
            </ul>
          </iv-sidebar-footer>
          <iv-sidebar-rail />
        </iv-sidebar>
        <main ivSidebarInset class="min-w-0">
          <header class="flex h-12 shrink-0 items-center gap-2 border-b px-3">
            <iv-sidebar-trigger />
            <span class="bg-border h-4 w-px shrink-0"></span>
            <span class="truncate text-sm font-medium">Dashboard</span>
            <div class="ml-auto flex gap-1" role="group" aria-label="Variant">
              @for (v of variants; track v) {
                <button ivButton size="xs" [variant]="variant() === v ? 'secondary' : 'ghost'" [attr.aria-pressed]="variant() === v" (click)="variant.set(v)">
                  {{ v === 'floating' ? 'Floating' : 'Inset' }}
                </button>
              }
            </div>
          </header>
          <div class="flex min-h-0 flex-1 flex-col gap-3 p-4">
            <div class="grid grid-cols-3 gap-3">
              @for (s of stats; track s.label) {
                <div class="bg-muted/50 rounded-xl p-3">
                  <div class="text-muted-foreground truncate text-xs">{{ s.label }}</div>
                  <div class="mt-1 text-lg font-semibold tabular-nums">{{ s.value }}</div>
                </div>
              }
            </div>
            <div class="bg-muted/50 flex-1 rounded-xl"></div>
          </div>
        </main>
      </iv-sidebar-provider>
    </div>
  `,
})
export default class SidebarFloatingAndInsetDemo {
  protected readonly projects = ['Website redesign', 'Mobile app', 'Brand refresh'];
  protected readonly stats = [
    { label: 'Open issues', value: '24' },
    { label: 'In review', value: '7' },
    { label: 'Shipped', value: '132' },
  ];
  protected readonly variants = ['floating', 'inset'] as const;
  protected readonly variant = signal<'floating' | 'inset'>('inset');
}

Right side

Set side="right" and place iv-sidebar after main[ivSidebarInset], so its layout gap is on the right.

Open issues
24
In review
7
Shipped
132
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LucideLayoutDashboard, LucideInbox, LucideFolderKanban, LucideCalendar, LucideUsers, LucidePlus, LucideEllipsis, LucideChevronsUpDown, LucideSparkles } from '@lucide/angular';
import { IV_SIDEBAR_IMPORTS } from '@inlaylab/ivorie/sidebar';

// The sidebar sits on the right: place it after the inset in markup, and the trigger on that side too.
// cookieName="" so this demo never saves its state over the site's own sidebar.
@Component({
  selector: 'demo-sidebar-right-side',
  imports: [IV_SIDEBAR_IMPORTS, LucideLayoutDashboard, LucideInbox, LucideFolderKanban, LucideCalendar, LucideUsers, LucidePlus, LucideEllipsis, LucideChevronsUpDown, LucideSparkles],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="relative h-[560px] w-full transform-gpu overflow-hidden rounded-xl border">
      <iv-sidebar-provider cookieName="" class="h-full min-h-0">
        <main ivSidebarInset class="min-w-0">
          <header class="flex h-12 shrink-0 items-center gap-2 border-b px-3">
            <nav aria-label="Breadcrumb" class="flex min-w-0 items-center gap-1.5 text-sm">
              <span class="text-muted-foreground hidden sm:inline">Workspace</span>
              <span class="text-muted-foreground hidden sm:inline">/</span>
              <span class="truncate font-medium">Dashboard</span>
            </nav>
            <span class="bg-border ml-auto h-4 w-px shrink-0"></span>
            <iv-sidebar-trigger />
          </header>
          <div class="flex min-h-0 flex-1 flex-col gap-3 p-4">
            <div class="grid grid-cols-3 gap-3">
              @for (s of stats; track s.label) {
                <div class="bg-muted/50 rounded-xl p-3">
                  <div class="text-muted-foreground truncate text-xs">{{ s.label }}</div>
                  <div class="mt-1 text-lg font-semibold tabular-nums">{{ s.value }}</div>
                </div>
              }
            </div>
            <div class="bg-muted/50 flex-1 rounded-xl"></div>
          </div>
        </main>
        <iv-sidebar side="right" class="h-full">
          <iv-sidebar-header>
            <ul ivSidebarMenu>
              <li ivSidebarMenuItem>
                <iv-sidebar-menu-button size="lg" tooltip="Acme Inc">
                  <span class="bg-primary text-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg">
                    <svg lucideSparkles></svg>
                  </span>
                  <span class="grid flex-1 text-left leading-tight">
                    <span class="truncate font-medium">Acme Inc</span>
                    <span class="text-muted-foreground truncate text-xs">Pro plan</span>
                  </span>
                  <svg lucideChevronsUpDown class="text-muted-foreground ml-auto"></svg>
                </iv-sidebar-menu-button>
              </li>
            </ul>
          </iv-sidebar-header>
          <iv-sidebar-content>
            <iv-sidebar-group>
              <iv-sidebar-group-label>Workspace</iv-sidebar-group-label>
              <ul ivSidebarMenu>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button [isActive]="true" tooltip="Dashboard">
                    <svg lucideLayoutDashboard></svg><span>Dashboard</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Inbox">
                    <svg lucideInbox></svg><span>Inbox</span>
                  </iv-sidebar-menu-button>
                  <iv-sidebar-menu-badge>12</iv-sidebar-menu-badge>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Projects">
                    <svg lucideFolderKanban></svg><span>Projects</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Calendar">
                    <svg lucideCalendar></svg><span>Calendar</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Team">
                    <svg lucideUsers></svg><span>Team</span>
                  </iv-sidebar-menu-button>
                </li>
              </ul>
            </iv-sidebar-group>
            <iv-sidebar-group>
              <iv-sidebar-group-label>Projects</iv-sidebar-group-label>
              <iv-sidebar-group-action title="Add project">
                <svg lucidePlus></svg><span class="sr-only">Add project</span>
              </iv-sidebar-group-action>
              <ul ivSidebarMenu>
                @for (p of projects; track p) {
                  <li ivSidebarMenuItem>
                    <iv-sidebar-menu-button [tooltip]="p">
                      <span class="bg-muted-foreground/40 size-2 shrink-0 rounded-full"></span><span>{{ p }}</span>
                    </iv-sidebar-menu-button>
                    <iv-sidebar-menu-action [showOnHover]="true" title="More">
                      <svg lucideEllipsis></svg><span class="sr-only">More for {{ p }}</span>
                    </iv-sidebar-menu-action>
                  </li>
                }
              </ul>
            </iv-sidebar-group>
          </iv-sidebar-content>
          <iv-sidebar-footer>
            <ul ivSidebarMenu>
              <li ivSidebarMenuItem>
                <iv-sidebar-menu-button size="lg" tooltip="Maya Ross">
                  <span class="bg-muted flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-medium">MR</span>
                  <span class="grid flex-1 text-left leading-tight">
                    <span class="truncate font-medium">Maya Ross</span>
                    <span class="text-muted-foreground truncate text-xs">maya&#64;acme.dev</span>
                  </span>
                  <svg lucideChevronsUpDown class="text-muted-foreground ml-auto"></svg>
                </iv-sidebar-menu-button>
              </li>
            </ul>
          </iv-sidebar-footer>
          <iv-sidebar-rail />
        </iv-sidebar>
      </iv-sidebar-provider>
    </div>
  `,
})
export default class SidebarRightSideDemo {
  protected readonly projects = ['Website redesign', 'Mobile app', 'Brand refresh'];
  protected readonly stats = [
    { label: 'Open issues', value: '24' },
    { label: 'In review', value: '7' },
    { label: 'Shipped', value: '132' },
  ];
}
Open issues
24
In review
7
Shipped
132
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LucideLayoutDashboard, LucideInbox, LucideFolderKanban, LucideCalendar, LucideUsers, LucideChevronsUpDown, LucideSparkles } from '@lucide/angular';
import { IV_SIDEBAR_IMPORTS } from '@inlaylab/ivorie/sidebar';

// A nested list under a menu item, with the current page marked.
// cookieName="" so this demo never saves its state over the site's own sidebar.
@Component({
  selector: 'demo-sidebar-sub-menus',
  imports: [IV_SIDEBAR_IMPORTS, LucideLayoutDashboard, LucideInbox, LucideFolderKanban, LucideCalendar, LucideUsers, LucideChevronsUpDown, LucideSparkles],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="relative h-[560px] w-full transform-gpu overflow-hidden rounded-xl border">
      <iv-sidebar-provider cookieName="" class="h-full min-h-0">
        <iv-sidebar collapsible="offcanvas" class="h-full">
          <iv-sidebar-header>
            <ul ivSidebarMenu>
              <li ivSidebarMenuItem>
                <iv-sidebar-menu-button size="lg" tooltip="Acme Inc">
                  <span class="bg-primary text-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg">
                    <svg lucideSparkles></svg>
                  </span>
                  <span class="grid flex-1 text-left leading-tight">
                    <span class="truncate font-medium">Acme Inc</span>
                    <span class="text-muted-foreground truncate text-xs">Pro plan</span>
                  </span>
                  <svg lucideChevronsUpDown class="text-muted-foreground ml-auto"></svg>
                </iv-sidebar-menu-button>
              </li>
            </ul>
          </iv-sidebar-header>
          <iv-sidebar-content>
            <iv-sidebar-group>
              <iv-sidebar-group-label>Workspace</iv-sidebar-group-label>
              <ul ivSidebarMenu>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button [isActive]="true" tooltip="Dashboard">
                    <svg lucideLayoutDashboard></svg><span>Dashboard</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Inbox">
                    <svg lucideInbox></svg><span>Inbox</span>
                  </iv-sidebar-menu-button>
                  <iv-sidebar-menu-badge>12</iv-sidebar-menu-badge>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Projects">
                    <svg lucideFolderKanban></svg><span>Projects</span>
                  </iv-sidebar-menu-button>
                  <ul ivSidebarMenuSub>
                    @for (p of projects; track p) {
                      <li ivSidebarMenuSubItem>
                        <a ivSidebarMenuSubButton href="#" [isActive]="p === 'Website redesign'" (click)="$event.preventDefault()">
                          <span>{{ p }}</span>
                        </a>
                      </li>
                    }
                  </ul>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Calendar">
                    <svg lucideCalendar></svg><span>Calendar</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Team">
                    <svg lucideUsers></svg><span>Team</span>
                  </iv-sidebar-menu-button>
                </li>
              </ul>
            </iv-sidebar-group>
          </iv-sidebar-content>
          <iv-sidebar-footer>
            <ul ivSidebarMenu>
              <li ivSidebarMenuItem>
                <iv-sidebar-menu-button size="lg" tooltip="Maya Ross">
                  <span class="bg-muted flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-medium">MR</span>
                  <span class="grid flex-1 text-left leading-tight">
                    <span class="truncate font-medium">Maya Ross</span>
                    <span class="text-muted-foreground truncate text-xs">maya&#64;acme.dev</span>
                  </span>
                  <svg lucideChevronsUpDown class="text-muted-foreground ml-auto"></svg>
                </iv-sidebar-menu-button>
              </li>
            </ul>
          </iv-sidebar-footer>
          <iv-sidebar-rail />
        </iv-sidebar>
        <main ivSidebarInset class="min-w-0">
          <header class="flex h-12 shrink-0 items-center gap-2 border-b px-3">
            <iv-sidebar-trigger />
            <span class="bg-border h-4 w-px shrink-0"></span>
            <nav aria-label="Breadcrumb" class="flex min-w-0 items-center gap-1.5 text-sm">
              <span class="text-muted-foreground hidden sm:inline">Workspace</span>
              <span class="text-muted-foreground hidden sm:inline">/</span>
              <span class="truncate font-medium">Dashboard</span>
            </nav>
          </header>
          <div class="flex min-h-0 flex-1 flex-col gap-3 p-4">
            <div class="grid grid-cols-3 gap-3">
              @for (s of stats; track s.label) {
                <div class="bg-muted/50 rounded-xl p-3">
                  <div class="text-muted-foreground truncate text-xs">{{ s.label }}</div>
                  <div class="mt-1 text-lg font-semibold tabular-nums">{{ s.value }}</div>
                </div>
              }
            </div>
            <div class="bg-muted/50 flex-1 rounded-xl"></div>
          </div>
        </main>
      </iv-sidebar-provider>
    </div>
  `,
})
export default class SidebarSubMenusDemo {
  protected readonly projects = ['Website redesign', 'Mobile app', 'Brand refresh'];
  protected readonly stats = [
    { label: 'Open issues', value: '24' },
    { label: 'In review', value: '7' },
    { label: 'Shipped', value: '132' },
  ];
}

Programmatic control

Expanded
Open issues
24
In review
7
Shipped
132
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LucideLayoutDashboard, LucideInbox, LucideFolderKanban, LucideCalendar, LucideUsers, LucidePlus, LucideEllipsis, LucideChevronsUpDown, LucideSparkles } from '@lucide/angular';
import { IvButton } from '@inlaylab/ivorie/button';
import { IV_SIDEBAR_IMPORTS } from '@inlaylab/ivorie/sidebar';

// Read and set the state through the sidebar service on the provider.
// cookieName="" so this demo never saves its state over the site's own sidebar.
@Component({
  selector: 'demo-sidebar-programmatic-control',
  imports: [IV_SIDEBAR_IMPORTS, IvButton, LucideLayoutDashboard, LucideInbox, LucideFolderKanban, LucideCalendar, LucideUsers, LucidePlus, LucideEllipsis, LucideChevronsUpDown, LucideSparkles],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="relative h-[560px] w-full transform-gpu overflow-hidden rounded-xl border">
      <iv-sidebar-provider cookieName="" #provider class="h-full min-h-0">
        <iv-sidebar collapsible="icon" class="h-full">
          <iv-sidebar-header>
            <ul ivSidebarMenu>
              <li ivSidebarMenuItem>
                <iv-sidebar-menu-button size="lg" tooltip="Acme Inc">
                  <span class="bg-primary text-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg">
                    <svg lucideSparkles></svg>
                  </span>
                  <span class="grid flex-1 text-left leading-tight">
                    <span class="truncate font-medium">Acme Inc</span>
                    <span class="text-muted-foreground truncate text-xs">Pro plan</span>
                  </span>
                  <svg lucideChevronsUpDown class="text-muted-foreground ml-auto"></svg>
                </iv-sidebar-menu-button>
              </li>
            </ul>
          </iv-sidebar-header>
          <iv-sidebar-content>
            <iv-sidebar-group>
              <iv-sidebar-group-label>Workspace</iv-sidebar-group-label>
              <ul ivSidebarMenu>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button [isActive]="true" tooltip="Dashboard">
                    <svg lucideLayoutDashboard></svg><span>Dashboard</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Inbox">
                    <svg lucideInbox></svg><span>Inbox</span>
                  </iv-sidebar-menu-button>
                  <iv-sidebar-menu-badge>12</iv-sidebar-menu-badge>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Projects">
                    <svg lucideFolderKanban></svg><span>Projects</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Calendar">
                    <svg lucideCalendar></svg><span>Calendar</span>
                  </iv-sidebar-menu-button>
                </li>
                <li ivSidebarMenuItem>
                  <iv-sidebar-menu-button tooltip="Team">
                    <svg lucideUsers></svg><span>Team</span>
                  </iv-sidebar-menu-button>
                </li>
              </ul>
            </iv-sidebar-group>
            <iv-sidebar-group>
              <iv-sidebar-group-label>Projects</iv-sidebar-group-label>
              <iv-sidebar-group-action title="Add project">
                <svg lucidePlus></svg><span class="sr-only">Add project</span>
              </iv-sidebar-group-action>
              <ul ivSidebarMenu>
                @for (p of projects; track p) {
                  <li ivSidebarMenuItem>
                    <iv-sidebar-menu-button [tooltip]="p">
                      <span class="bg-muted-foreground/40 size-2 shrink-0 rounded-full"></span><span>{{ p }}</span>
                    </iv-sidebar-menu-button>
                    <iv-sidebar-menu-action [showOnHover]="true" title="More">
                      <svg lucideEllipsis></svg><span class="sr-only">More for {{ p }}</span>
                    </iv-sidebar-menu-action>
                  </li>
                }
              </ul>
            </iv-sidebar-group>
          </iv-sidebar-content>
          <iv-sidebar-footer>
            <ul ivSidebarMenu>
              <li ivSidebarMenuItem>
                <iv-sidebar-menu-button size="lg" tooltip="Maya Ross">
                  <span class="bg-muted flex size-8 shrink-0 items-center justify-center rounded-full text-xs font-medium">MR</span>
                  <span class="grid flex-1 text-left leading-tight">
                    <span class="truncate font-medium">Maya Ross</span>
                    <span class="text-muted-foreground truncate text-xs">maya&#64;acme.dev</span>
                  </span>
                  <svg lucideChevronsUpDown class="text-muted-foreground ml-auto"></svg>
                </iv-sidebar-menu-button>
              </li>
            </ul>
          </iv-sidebar-footer>
          <iv-sidebar-rail />
        </iv-sidebar>
        <main ivSidebarInset class="min-w-0">
          <header class="flex h-12 shrink-0 items-center gap-2 border-b px-3">
            <iv-sidebar-trigger />
            <span class="bg-border h-4 w-px shrink-0"></span>
            <span class="text-muted-foreground truncate text-sm" aria-live="polite">{{ provider.sidebar.state() === 'expanded' ? 'Expanded' : 'Collapsed' }}</span>
            <div class="ml-auto flex gap-1">
              <button ivButton variant="outline" size="xs" [disabled]="provider.sidebar.state() === 'collapsed'" (click)="provider.sidebar.setOpen(false)">Collapse</button>
              <button ivButton variant="outline" size="xs" [disabled]="provider.sidebar.state() === 'expanded'" (click)="provider.sidebar.setOpen(true)">Expand</button>
            </div>
          </header>
          <div class="flex min-h-0 flex-1 flex-col gap-3 p-4">
            <div class="grid grid-cols-3 gap-3">
              @for (s of stats; track s.label) {
                <div class="bg-muted/50 rounded-xl p-3">
                  <div class="text-muted-foreground truncate text-xs">{{ s.label }}</div>
                  <div class="mt-1 text-lg font-semibold tabular-nums">{{ s.value }}</div>
                </div>
              }
            </div>
            <div class="bg-muted/50 flex-1 rounded-xl"></div>
          </div>
        </main>
      </iv-sidebar-provider>
    </div>
  `,
})
export default class SidebarProgrammaticControlDemo {
  protected readonly projects = ['Website redesign', 'Mobile app', 'Brand refresh'];
  protected readonly stats = [
    { label: 'Open issues', value: '24' },
    { label: 'In review', value: '7' },
    { label: 'Shipped', value: '132' },
  ];
}

API

Every part also accepts class, except where noted.

iv-sidebar-provider

Root. Provides IvSidebarService (one per provider), sets --sidebar-width: 16rem and --sidebar-width-icon: 3rem, and listens for Ctrl+B / Cmd+B.

Name Type Default Description
defaultOpen boolean true Open state when no sidebar_state cookie is saved. A saved cookie wins.
cookieName string 'sidebar_state' Cookie that saves open/closed. Give each sidebar on a page its own name; '' saves nothing.

iv-sidebar

Name Type Default Description
side 'left' | 'right' 'left' Edge the sidebar sits on.
variant 'sidebar' | 'floating' | 'inset' 'sidebar' sidebar fills the edge. floating and inset are padded and rounded.
collapsible 'offcanvas' | 'icon' | 'none' 'offcanvas' offcanvas slides out. icon shrinks to icon width. none is always fixed open and never a drawer.

class goes on the fixed container on desktop, and on the panel when collapsible="none".

iv-sidebar-trigger

Icon button that calls toggle(). Has an aria-expanded state and a screen-reader label "Toggle Sidebar".

iv-sidebar-rail

Thin clickable strip on the sidebar edge. Toggles the sidebar. No inputs, no class.

main[ivSidebarInset]

Main content area next to the sidebar. Also matches any [ivSidebarInset].

Top, bottom and scrollable middle regions. No inputs, no class.

iv-sidebar-separator

Horizontal rule with role="separator". No inputs, no class.

iv-sidebar-group, iv-sidebar-group-content

Group wrapper and its content area. No inputs, no class.

iv-sidebar-group-label

Muted group heading. Hidden when collapsed to icons. No own inputs.

iv-sidebar-group-action

Action button in the group header. Hidden when collapsed to icons. No own inputs.

ul[ivSidebarMenu], li[ivSidebarMenuItem]

Menu list and item. No inputs, no class.

iv-sidebar-menu-button

Name Type Default Description
isActive boolean false Marks the current page. Sets data-active and aria-current="page".
variant 'default' | 'outline' 'default' Button style.
size 'default' | 'sm' | 'lg' 'default' Height.
tooltip string '' Label shown on hover only while the sidebar is collapsed to icons on desktop.

a[ivSidebarMenuButton], button[ivSidebarMenuButton]

The same look on your own element: a link for routerLink, or a native button for a dropdown or collapsible trigger. Takes isActive, variant and size, no tooltip.

<a ivSidebarMenuButton routerLink="/inbox" routerLinkActive #rla="routerLinkActive" [isActive]="rla.isActive">Inbox</a>

iv-sidebar-menu-action

Name Type Default Description
showOnHover boolean false Hide until the item is hovered or focused (from md up).

iv-sidebar-menu-badge

Count or label at the item end. Hidden when collapsed to icons. No own inputs.

iv-sidebar-menu-skeleton

Loading placeholder with a deterministic width, so server and client match. No class.

Name Type Default Description
showIcon boolean false Show an icon placeholder.

ul[ivSidebarMenuSub], li[ivSidebarMenuSubItem]

Nested list and item. Sub-menus hide when collapsed to icons. No inputs, no class.

a[ivSidebarMenuSubButton]

Name Type Default Description
size 'sm' | 'md' 'md' Text size.
isActive boolean false Marks the current page. Sets data-active and aria-current="page".

IvSidebarService

Inject it inside a provider to read or drive the sidebar. State is signals.

Name Type Description
state Signal<'expanded' | 'collapsed'> Desktop state.
open WritableSignal<boolean> Desktop open state. Starts from the cookie, else defaultOpen.
openMobile WritableSignal<boolean> Drawer open state.
isMobile WritableSignal<boolean> True below 768px.
expanded Signal<boolean> Open state of the form showing: drawer on mobile, panel on desktop.
toggle() () => void Flip the visible form.
setOpen(value) (boolean) => void Set the visible form. On desktop it also writes the cookie.

Accessibility

  • Ctrl+B or Cmd+B toggles the sidebar from anywhere on the page.
  • On mobile the sidebar is a modal drawer with role="dialog", aria-modal="true" and aria-label="Sidebar".
  • Focus is trapped in the open drawer and moves into it. Escape or a click on the backdrop closes it. Focus returns to the element that had it before.
  • The trigger is a <button> with aria-expanded and aria-controls (the sidebar id).
  • The rail is a button with aria-label="Toggle Sidebar" and is out of the Tab order.
  • The active menu button and sub button have aria-current="page".

Notes

  • Mobile breakpoint is max-width: 767px. The viewport is checked after the first render.
  • To render the server HTML in mobile mode already, set provideIvorie({ sidebarMobile: true }), for example from a user-agent hint.
  • The desktop state is saved in the sidebar_state cookie for 7 days. The server reads it from the request Cookie header, so the first HTML has the right state. The cookie is written only in the browser. The mobile drawer is not saved.
  • Colors come from the --sidebar* tokens in projects/ui/styles/theme.css, which map to the main tokens by default.
  • Each provider has its own service. Nested providers are independent sidebars.
  • sidebarMenuButtonVariants and SidebarMenuButtonVariants are exported.