Sidebar
A collapsible app sidebar with a mobile drawer and saved state.
import { โฆ } from '@inlaylab/ivorie/sidebar'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@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
npx ivorie add sidebarimport { โฆ } from '@inlaylab/ivorie/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
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@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
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@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.
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@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' },
];
}
Sub-menus
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@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
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@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].
iv-sidebar-header, iv-sidebar-footer, iv-sidebar-content
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"andaria-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>witharia-expandedandaria-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_statecookie for 7 days. The server reads it from the requestCookieheader, 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 inprojects/ui/styles/theme.css, which map to the main tokens by default. - Each provider has its own service. Nested providers are independent sidebars.
sidebarMenuButtonVariantsandSidebarMenuButtonVariantsare exported.