Layout
Sidebar
Usage#
import { WDS_SIDEBAR } from '@wds/angular/sidebar';Examples#
Default#
wds-sidebar-provider holds the state and the ⌘/Ctrl+B shortcut; wds-sidebar-inset is the page next to the sidebar. With collapsible="icon" only the icons stay, and tooltip shows the names. Toggle it with wds-sidebar-trigger, a click on the edge (wds-sidebar-rail) or the shortcut. Below 768 px the sidebar opens as a panel over the content. wds-sidebar-menu-button can be the trigger of wds-collapsible (submenus) and wds-dropdown (team switcher, user menu).
import { Component, signal } from '@angular/core';
import { WDS_AVATAR } from '@wds/angular/avatar';
import { WDS_BREADCRUMB } from '@wds/angular/breadcrumb';
import { WDS_COLLAPSIBLE } from '@wds/angular/collapsible';
import { WDS_DROPDOWN } from '@wds/angular/dropdown';
import { WDS_SIDEBAR } from '@wds/angular/sidebar';
import { Icon } from './icon';
const TEAMS = [
{ name: 'Acme Inc', plan: 'Enterprise', icon: 'galleryVerticalEnd' },
{ name: 'Acme Corp.', plan: 'Startup', icon: 'audioWaveform' },
{ name: 'Evil Corp.', plan: 'Free', icon: 'command' },
];
const NAV = [
{ title: 'Playground', icon: 'squareTerminal', pages: ['History', 'Starred', 'Settings'] },
{ title: 'Models', icon: 'bot', pages: ['Genesis', 'Explorer', 'Quantum'] },
{ title: 'Documentation', icon: 'bookOpen', pages: ['Introduction', 'Get Started', 'Tutorials', 'Changelog'] },
{ title: 'Settings', icon: 'settings', pages: ['General', 'Team', 'Billing', 'Limits'] },
];
const PROJECTS = [
{ name: 'Design Engineering', icon: 'frame' },
{ name: 'Sales & Marketing', icon: 'pieChart' },
{ name: 'Travel', icon: 'map' },
];
@Component({
selector: 'app-sidebar-default-demo',
host: { class: 'block w-full' },
imports: [...WDS_SIDEBAR, ...WDS_AVATAR, ...WDS_COLLAPSIBLE, ...WDS_DROPDOWN, ...WDS_BREADCRUMB, Icon],
template: `
<wds-sidebar-provider class="h-[600px] min-h-0 rounded-xl border border-border">
<wds-sidebar collapsible="icon">
<wds-sidebar-header>
<wds-sidebar-menu>
<wds-sidebar-menu-item>
<wds-dropdown class="block" style="--wds-dropdown-min-width: 14rem" (wdsSelect)="selectTeam($event.value)">
<wds-sidebar-menu-button slot="trigger" size="lg" [tooltip]="team().name">
<span class="flex size-8 flex-none items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<svg [appIcon]="team().icon" />
</span>
<span class="grid flex-1 leading-tight">
<span class="truncate font-medium">{{ team().name }}</span>
<span class="truncate text-xs">{{ team().plan }}</span>
</span>
<svg class="ml-auto" appIcon="chevronsUpDown" />
</wds-sidebar-menu-button>
<wds-dropdown-label>Teams</wds-dropdown-label>
@for (t of teams; track t.name) {
<wds-dropdown-item [value]="t.name"><svg [appIcon]="t.icon" />{{ t.name }}</wds-dropdown-item>
}
<wds-dropdown-separator />
<wds-dropdown-item value="add"><svg appIcon="plus" />Add team</wds-dropdown-item>
</wds-dropdown>
</wds-sidebar-menu-item>
</wds-sidebar-menu>
</wds-sidebar-header>
<wds-sidebar-content>
<wds-sidebar-group>
<wds-sidebar-group-label>Platform</wds-sidebar-group-label>
<wds-sidebar-menu>
@for (item of nav; track item.title) {
<wds-sidebar-menu-item>
<!-- group-open rotates the chevron while the collapsible is open -->
<wds-collapsible class="group" [open]="$first">
<wds-collapsible-trigger>
<wds-sidebar-menu-button [tooltip]="item.title">
<svg [appIcon]="item.icon" />
<span>{{ item.title }}</span>
<svg class="ml-auto transition-transform duration-200 group-open:rotate-90" appIcon="chevronRight" />
</wds-sidebar-menu-button>
</wds-collapsible-trigger>
<wds-collapsible-content>
<wds-sidebar-menu-sub>
@for (page of item.pages; track page) {
<wds-sidebar-menu-sub-item>
<wds-sidebar-menu-sub-button [active]="isActive(item.title, page)" (click)="open(item.title, page)">
<span>{{ page }}</span>
</wds-sidebar-menu-sub-button>
</wds-sidebar-menu-sub-item>
}
</wds-sidebar-menu-sub>
</wds-collapsible-content>
</wds-collapsible>
</wds-sidebar-menu-item>
}
</wds-sidebar-menu>
</wds-sidebar-group>
<wds-sidebar-group>
<wds-sidebar-group-label>Projects</wds-sidebar-group-label>
<wds-sidebar-group-action label="Add project"><svg appIcon="plus" /></wds-sidebar-group-action>
<wds-sidebar-menu>
@for (project of projects; track project.name) {
<wds-sidebar-menu-item>
<wds-sidebar-menu-button [tooltip]="project.name" [active]="isActive('Projects', project.name)" (click)="open('Projects', project.name)">
<svg [appIcon]="project.icon" />
<span>{{ project.name }}</span>
</wds-sidebar-menu-button>
<wds-dropdown align="start">
<wds-sidebar-menu-action slot="trigger" show-on-hover [label]="'More: ' + project.name">
<svg appIcon="moreHorizontal" />
</wds-sidebar-menu-action>
<wds-dropdown-item value="view"><svg appIcon="folder" />View project</wds-dropdown-item>
<wds-dropdown-item value="share"><svg appIcon="forward" />Share project</wds-dropdown-item>
<wds-dropdown-separator />
<wds-dropdown-item value="delete" variant="destructive"><svg appIcon="trash" />Delete project</wds-dropdown-item>
</wds-dropdown>
</wds-sidebar-menu-item>
}
</wds-sidebar-menu>
</wds-sidebar-group>
</wds-sidebar-content>
<wds-sidebar-footer>
<wds-sidebar-menu>
<wds-sidebar-menu-item>
<wds-dropdown class="block" side="top" style="--wds-dropdown-min-width: 14rem">
<wds-sidebar-menu-button slot="trigger" size="lg" tooltip="Jane Doe">
<wds-avatar class="[--wds-avatar-radius:var(--wds-radius-lg)]" aria-hidden="true">JD</wds-avatar>
<span class="grid flex-1 leading-tight">
<span class="truncate font-medium">Jane Doe</span>
<span class="truncate text-xs">jane@example.com</span>
</span>
<svg class="ml-auto" appIcon="chevronsUpDown" />
</wds-sidebar-menu-button>
<wds-dropdown-label>My account</wds-dropdown-label>
<wds-dropdown-item value="account"><svg appIcon="user" />Account</wds-dropdown-item>
<wds-dropdown-item value="billing"><svg appIcon="creditCard" />Billing</wds-dropdown-item>
<wds-dropdown-item value="notifications"><svg appIcon="bell" />Notifications</wds-dropdown-item>
<wds-dropdown-separator />
<wds-dropdown-item value="logout"><svg appIcon="logOut" />Log out</wds-dropdown-item>
</wds-dropdown>
</wds-sidebar-menu-item>
</wds-sidebar-menu>
</wds-sidebar-footer>
<wds-sidebar-rail />
</wds-sidebar>
<wds-sidebar-inset>
<header class="box-border flex h-14 flex-none items-center gap-2 border-b border-border px-4">
<wds-sidebar-trigger class="-ml-1" />
<span class="mx-1 h-4 w-px bg-border"></span>
<wds-breadcrumb class="min-w-0" label="Breadcrumb" wrap="nowrap">
<wds-breadcrumb-item class="max-md:hidden">
<wds-breadcrumb-link (click)="openSection(active().section)">{{ active().section }}</wds-breadcrumb-link>
</wds-breadcrumb-item>
<wds-breadcrumb-item>
<wds-breadcrumb-page class="truncate">{{ active().page }}</wds-breadcrumb-page>
</wds-breadcrumb-item>
</wds-breadcrumb>
</header>
<div class="grid flex-1 grid-rows-[auto_1fr] gap-4 overflow-auto p-4">
<div class="grid grid-cols-3 gap-4">
<div class="aspect-video rounded-xl bg-muted"></div>
<div class="aspect-video rounded-xl bg-muted"></div>
<div class="aspect-video rounded-xl bg-muted"></div>
</div>
<div class="min-h-40 rounded-xl bg-muted"></div>
</div>
</wds-sidebar-inset>
</wds-sidebar-provider>
`,
})
export class SidebarDefaultDemo {
protected readonly teams = TEAMS;
protected readonly nav = NAV;
protected readonly projects = PROJECTS;
protected readonly team = signal(TEAMS[0]);
protected readonly active = signal({ section: 'Playground', page: 'History' });
protected selectTeam(name: string): void {
const team = TEAMS.find((t) => t.name === name);
if (team) this.team.set(team);
}
protected isActive(section: string, page: string): boolean {
const active = this.active();
return active.section === section && active.page === page;
}
protected open(section: string, page: string): void {
this.active.set({ section, page });
}
/** Breadcrumb link: the first page of the section. */
protected openSection(section: string): void {
this.open(section, NAV.find((item) => item.title === section)?.pages[0] ?? PROJECTS[0].name);
}
}import { Component, computed, input } from '@angular/core';
import { ICONS } from './icons';
/** `<svg appIcon="house" />`: a 16×16 lucide icon, hidden from screen readers. */
@Component({
selector: 'svg[appIcon]',
host: {
viewBox: '0 0 24 24',
width: '16',
height: '16',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'aria-hidden': 'true',
},
template: `@for (d of paths(); track $index) {<svg:path [attr.d]="d" />}`,
})
export class Icon {
readonly appIcon = input.required<string>();
protected readonly paths = computed(() => ICONS[this.appIcon()] ?? []);
}/** Lucide icons of the sidebar examples: the `d` of each `<path>` (viewBox 0 0 24 24). */
export const ICONS: Record<string, string[]> = {
galleryVerticalEnd: ['M7 2h10', 'M5 6h14', 'M5 10h14a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2z'],
audioWaveform: ['M2 13a2 2 0 0 0 2-2V7a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0V4a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0v-4a2 2 0 0 1 2-2'],
command: ['M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3'],
chevronsUpDown: ['m7 15 5 5 5-5', 'm7 9 5-5 5 5'],
chevronRight: ['m9 18 6-6-6-6'],
chevronDown: ['m6 9 6 6 6-6'],
squareTerminal: ['m7 11 2-2-2-2', 'M11 13h4', 'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z'],
bot: ['M12 8V4H8', 'M6 8h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2z', 'M2 14h2', 'M20 14h2', 'M15 13v2', 'M9 13v2'],
bookOpen: ['M12 7v14', 'M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z'],
settings: ['M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z', 'M12 9a3 3 0 1 0 0 6a3 3 0 1 0 0 -6'],
frame: ['M22 6H2', 'M22 18H2', 'M6 2v20', 'M18 2v20'],
pieChart: ['M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z', 'M21.21 15.89A10 10 0 1 1 8 2.83'],
map: ['M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z', 'M15 5.764v15', 'M9 3.236v15'],
moreHorizontal: ['M12 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2', 'M19 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2', 'M5 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2'],
plus: ['M5 12h14', 'M12 5v14'],
folder: ['M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z'],
forward: ['m15 17 5-5-5-5', 'M4 18v-2a4 4 0 0 1 4-4h12'],
trash: ['M3 6h18', 'M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6', 'M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2', 'M10 11v6', 'M14 11v6'],
user: ['M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2', 'M12 3a4 4 0 1 0 0 8a4 4 0 1 0 0 -8'],
creditCard: ['M4 5h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z', 'M2 10h20'],
bell: ['M10.268 21a2 2 0 0 0 3.464 0', 'M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326'],
logOut: ['m16 17 5-5-5-5', 'M21 12H9', 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'],
house: ['M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8', 'M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z'],
inbox: ['M22 12h-6l-2 3h-4l-2-3H2', 'M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z'],
calendar: ['M8 2v4', 'M16 2v4', 'M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z', 'M3 10h18'],
search: ['m21 21-4.34-4.34', 'M11 3a8 8 0 1 0 0 16a8 8 0 1 0 0 -16'],
building: ['M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z', 'M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2', 'M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2', 'M10 6h4', 'M10 10h4', 'M10 14h4', 'M10 18h4'],
users: ['M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2', 'M9 3a4 4 0 1 0 0 8a4 4 0 1 0 0 -8', 'M22 21v-2a4 4 0 0 0-3-3.87', 'M16 3.13a4 4 0 0 1 0 7.75'],
server: ['M4 2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z', 'M4 14h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2z', 'M6 6h.01', 'M6 18h.01'],
list: ['M3 12h.01', 'M3 18h.01', 'M3 6h.01', 'M8 12h13', 'M8 18h13', 'M8 6h13'],
send: ['m22 2-7 20-4-9-9-4Z', 'M22 2 11 13'],
key: ['m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4', 'm21 2-9.6 9.6', 'M7.5 10a5.5 5.5 0 1 0 0 11a5.5 5.5 0 1 0 0 -11'],
fileText: ['M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z', 'M14 2v4a2 2 0 0 0 2 2h4', 'M10 9H8', 'M16 13H8', 'M16 17H8'],
shield: ['M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z'],
layoutGrid: ['M4 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z', 'M15 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z', 'M15 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z', 'M4 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z'],
lifeBuoy: ['M12 2a10 10 0 1 0 0 20a10 10 0 1 0 0 -20', 'M12 8a4 4 0 1 0 0 8a4 4 0 1 0 0 -8', 'm4.93 4.93 4.24 4.24', 'm14.83 9.17 4.24-4.24', 'm14.83 14.83 4.24 4.24', 'm9.17 14.83-4.24 4.24'],
};Variants#
side sets the edge. variant: sidebar (at the edge), floating (a floating card) or inset (the content as a card). collapsible: offcanvas (slides out), icon (icons only) or none (always expanded). wds-sidebar-menu-badge adds a counter and variant="outline" a bordered button.
import { Component, signal } from '@angular/core';
import { WdsInputComponent } from '@wds/angular/input';
import { WDS_SELECT } from '@wds/angular/select';
import { WDS_SIDEBAR } from '@wds/angular/sidebar';
import type { WdsSidebarCollapsible, WdsSidebarSide, WdsSidebarVariant } from '@wds/core/sidebar';
import { Icon } from './icon';
const ITEMS = [
{ title: 'Home', icon: 'house' },
{ title: 'Inbox', icon: 'inbox', badge: '24' },
{ title: 'Calendar', icon: 'calendar' },
{ title: 'Search', icon: 'search' },
{ title: 'Settings', icon: 'settings' },
];
@Component({
selector: 'app-sidebar-variants-demo',
host: { class: 'flex w-full flex-col gap-3' },
imports: [...WDS_SIDEBAR, ...WDS_SELECT, WdsInputComponent, Icon],
template: `
<div class="flex flex-wrap items-center gap-3">
<wds-select class="w-36" aria-label="Side" [value]="side()" (wdsChange)="side.set($any($event.value))">
<wds-select-item value="left">side: left</wds-select-item>
<wds-select-item value="right">side: right</wds-select-item>
</wds-select>
<wds-select class="w-44" aria-label="Variant" [value]="variant()" (wdsChange)="variant.set($any($event.value))">
<wds-select-item value="sidebar">variant: sidebar</wds-select-item>
<wds-select-item value="floating">variant: floating</wds-select-item>
<wds-select-item value="inset">variant: inset</wds-select-item>
</wds-select>
<wds-select class="w-52" aria-label="Collapsible" [value]="collapsible()" (wdsChange)="collapsible.set($any($event.value))">
<wds-select-item value="offcanvas">collapsible: offcanvas</wds-select-item>
<wds-select-item value="icon">collapsible: icon</wds-select-item>
<wds-select-item value="none">collapsible: none</wds-select-item>
</wds-select>
</div>
<!-- shortcut="" turns off ⌘/Ctrl+B: only one sidebar on a page should have it -->
<wds-sidebar-provider class="h-[420px] min-h-0 rounded-xl border border-border" shortcut="">
<wds-sidebar [side]="side()" [variant]="variant()" [collapsible]="collapsible()">
<wds-sidebar-header>
<wds-input class="w-full" type="search" placeholder="Search…" aria-label="Search the menu" />
</wds-sidebar-header>
<wds-sidebar-content>
<wds-sidebar-group>
<wds-sidebar-group-label>Application</wds-sidebar-group-label>
<wds-sidebar-group-content>
<wds-sidebar-menu>
@for (item of items; track item.title) {
<wds-sidebar-menu-item>
<wds-sidebar-menu-button [tooltip]="item.title" [active]="active() === item.title" (click)="active.set(item.title)">
<svg [appIcon]="item.icon" />
<span>{{ item.title }}</span>
</wds-sidebar-menu-button>
@if (item.badge) {
<wds-sidebar-menu-badge>{{ item.badge }}</wds-sidebar-menu-badge>
}
</wds-sidebar-menu-item>
}
</wds-sidebar-menu>
</wds-sidebar-group-content>
</wds-sidebar-group>
</wds-sidebar-content>
<wds-sidebar-separator />
<wds-sidebar-footer>
<wds-sidebar-menu>
<wds-sidebar-menu-item>
<wds-sidebar-menu-button variant="outline" tooltip="Log out"><svg appIcon="logOut" /><span>Log out</span></wds-sidebar-menu-button>
</wds-sidebar-menu-item>
</wds-sidebar-menu>
</wds-sidebar-footer>
<wds-sidebar-rail />
</wds-sidebar>
<wds-sidebar-inset>
<header class="flex h-12 flex-none items-center gap-2 border-b border-border px-3" [class.flex-row-reverse]="side() === 'right'">
<wds-sidebar-trigger />
<span class="text-sm">{{ active() }}</span>
</header>
<div class="flex-1 p-4"><div class="h-full rounded-xl bg-muted"></div></div>
</wds-sidebar-inset>
</wds-sidebar-provider>
`,
})
export class SidebarVariantsDemo {
protected readonly items = ITEMS;
protected readonly side = signal<WdsSidebarSide>('left');
protected readonly variant = signal<WdsSidebarVariant>('sidebar');
protected readonly collapsible = signal<WdsSidebarCollapsible>('offcanvas');
protected readonly active = signal('Home');
}import { Component, computed, input } from '@angular/core';
import { ICONS } from './icons';
/** `<svg appIcon="house" />`: a 16×16 lucide icon, hidden from screen readers. */
@Component({
selector: 'svg[appIcon]',
host: {
viewBox: '0 0 24 24',
width: '16',
height: '16',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'aria-hidden': 'true',
},
template: `@for (d of paths(); track $index) {<svg:path [attr.d]="d" />}`,
})
export class Icon {
readonly appIcon = input.required<string>();
protected readonly paths = computed(() => ICONS[this.appIcon()] ?? []);
}/** Lucide icons of the sidebar examples: the `d` of each `<path>` (viewBox 0 0 24 24). */
export const ICONS: Record<string, string[]> = {
galleryVerticalEnd: ['M7 2h10', 'M5 6h14', 'M5 10h14a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2z'],
audioWaveform: ['M2 13a2 2 0 0 0 2-2V7a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0V4a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0v-4a2 2 0 0 1 2-2'],
command: ['M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3'],
chevronsUpDown: ['m7 15 5 5 5-5', 'm7 9 5-5 5 5'],
chevronRight: ['m9 18 6-6-6-6'],
chevronDown: ['m6 9 6 6 6-6'],
squareTerminal: ['m7 11 2-2-2-2', 'M11 13h4', 'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z'],
bot: ['M12 8V4H8', 'M6 8h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2z', 'M2 14h2', 'M20 14h2', 'M15 13v2', 'M9 13v2'],
bookOpen: ['M12 7v14', 'M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z'],
settings: ['M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z', 'M12 9a3 3 0 1 0 0 6a3 3 0 1 0 0 -6'],
frame: ['M22 6H2', 'M22 18H2', 'M6 2v20', 'M18 2v20'],
pieChart: ['M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z', 'M21.21 15.89A10 10 0 1 1 8 2.83'],
map: ['M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z', 'M15 5.764v15', 'M9 3.236v15'],
moreHorizontal: ['M12 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2', 'M19 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2', 'M5 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2'],
plus: ['M5 12h14', 'M12 5v14'],
folder: ['M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z'],
forward: ['m15 17 5-5-5-5', 'M4 18v-2a4 4 0 0 1 4-4h12'],
trash: ['M3 6h18', 'M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6', 'M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2', 'M10 11v6', 'M14 11v6'],
user: ['M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2', 'M12 3a4 4 0 1 0 0 8a4 4 0 1 0 0 -8'],
creditCard: ['M4 5h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z', 'M2 10h20'],
bell: ['M10.268 21a2 2 0 0 0 3.464 0', 'M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326'],
logOut: ['m16 17 5-5-5-5', 'M21 12H9', 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'],
house: ['M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8', 'M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z'],
inbox: ['M22 12h-6l-2 3h-4l-2-3H2', 'M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z'],
calendar: ['M8 2v4', 'M16 2v4', 'M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z', 'M3 10h18'],
search: ['m21 21-4.34-4.34', 'M11 3a8 8 0 1 0 0 16a8 8 0 1 0 0 -16'],
building: ['M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z', 'M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2', 'M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2', 'M10 6h4', 'M10 10h4', 'M10 14h4', 'M10 18h4'],
users: ['M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2', 'M9 3a4 4 0 1 0 0 8a4 4 0 1 0 0 -8', 'M22 21v-2a4 4 0 0 0-3-3.87', 'M16 3.13a4 4 0 0 1 0 7.75'],
server: ['M4 2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z', 'M4 14h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2z', 'M6 6h.01', 'M6 18h.01'],
list: ['M3 12h.01', 'M3 18h.01', 'M3 6h.01', 'M8 12h13', 'M8 18h13', 'M8 6h13'],
send: ['m22 2-7 20-4-9-9-4Z', 'M22 2 11 13'],
key: ['m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4', 'm21 2-9.6 9.6', 'M7.5 10a5.5 5.5 0 1 0 0 11a5.5 5.5 0 1 0 0 -11'],
fileText: ['M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z', 'M14 2v4a2 2 0 0 0 2 2h4', 'M10 9H8', 'M16 13H8', 'M16 17H8'],
shield: ['M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z'],
layoutGrid: ['M4 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z', 'M15 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z', 'M15 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z', 'M4 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z'],
lifeBuoy: ['M12 2a10 10 0 1 0 0 20a10 10 0 1 0 0 -20', 'M12 8a4 4 0 1 0 0 8a4 4 0 1 0 0 -8', 'm4.93 4.93 4.24 4.24', 'm14.83 9.17 4.24-4.24', 'm14.83 14.83 4.24 4.24', 'm9.17 14.83-4.24 4.24'],
};Collapsible group#
wds-collapsible-trigger, wds-sidebar-group-label becomes a button, and the group content in wds-collapsible-content expands smoothly. group-open: on the chevron follows the open attribute of wds-collapsible.
import { Component } from '@angular/core';
import { WdsSidebarComponent, WdsSidebarContentComponent, WdsSidebarGroupComponent, WdsSidebarGroupContentComponent, WdsSidebarGroupLabelComponent, WdsSidebarInsetComponent, WdsSidebarMenuButtonComponent, WdsSidebarMenuComponent, WdsSidebarMenuItemComponent, WdsSidebarProviderComponent } from '@wds/angular/sidebar';
import { WdsCollapsibleComponent, WdsCollapsibleContentComponent, WdsCollapsibleTriggerComponent } from '@wds/angular/collapsible';
@Component({
selector: 'app-sidebar-collapsible-group-demo',
imports: [WdsSidebarProviderComponent, WdsSidebarComponent, WdsSidebarContentComponent, WdsSidebarGroupComponent, WdsSidebarGroupLabelComponent, WdsSidebarGroupContentComponent, WdsSidebarMenuComponent, WdsSidebarMenuItemComponent, WdsSidebarMenuButtonComponent, WdsSidebarInsetComponent, WdsCollapsibleComponent, WdsCollapsibleTriggerComponent, WdsCollapsibleContentComponent],
template: `
<wds-sidebar-provider class="h-[360px] min-h-0 rounded-xl border border-border" shortcut="">
<wds-sidebar collapsible="none">
<wds-sidebar-content>
<wds-collapsible class="group" open>
<wds-sidebar-group>
<wds-collapsible-trigger>
<wds-sidebar-group-label>
Help
<svg class="ml-auto transition-transform duration-200 group-open:rotate-180" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
</wds-sidebar-group-label>
</wds-collapsible-trigger>
<wds-collapsible-content>
<wds-sidebar-group-content>
<wds-sidebar-menu>
<wds-sidebar-menu-item><wds-sidebar-menu-button><span>Documentation</span></wds-sidebar-menu-button></wds-sidebar-menu-item>
<wds-sidebar-menu-item><wds-sidebar-menu-button><span>Contact</span></wds-sidebar-menu-button></wds-sidebar-menu-item>
<wds-sidebar-menu-item><wds-sidebar-menu-button><span>Service status</span></wds-sidebar-menu-button></wds-sidebar-menu-item>
</wds-sidebar-menu>
</wds-sidebar-group-content>
</wds-collapsible-content>
</wds-sidebar-group>
</wds-collapsible>
<wds-collapsible class="group">
<wds-sidebar-group>
<wds-collapsible-trigger>
<wds-sidebar-group-label>
Account
<svg class="ml-auto transition-transform duration-200 group-open:rotate-180" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6" /></svg>
</wds-sidebar-group-label>
</wds-collapsible-trigger>
<wds-collapsible-content>
<wds-sidebar-group-content>
<wds-sidebar-menu>
<wds-sidebar-menu-item><wds-sidebar-menu-button><span>Profile</span></wds-sidebar-menu-button></wds-sidebar-menu-item>
<wds-sidebar-menu-item><wds-sidebar-menu-button><span>Security</span></wds-sidebar-menu-button></wds-sidebar-menu-item>
<wds-sidebar-menu-item><wds-sidebar-menu-button><span>Billing</span></wds-sidebar-menu-button></wds-sidebar-menu-item>
</wds-sidebar-menu>
</wds-sidebar-group-content>
</wds-collapsible-content>
</wds-sidebar-group>
</wds-collapsible>
</wds-sidebar-content>
</wds-sidebar>
<wds-sidebar-inset><div class="m-4 flex-1 rounded-xl bg-muted"></div></wds-sidebar-inset>
</wds-sidebar-provider>
`,
})
export class SidebarCollapsibleGroupDemo {}Menu skeleton#
wds-sidebar-menu-skeleton stands in for the items while the menu loads; show-icon adds an icon placeholder.
import { Component, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_SIDEBAR } from '@wds/angular/sidebar';
import { Icon } from './icon';
const PROJECTS = [
{ name: 'Design Engineering', icon: 'frame' },
{ name: 'Sales & Marketing', icon: 'pieChart' },
{ name: 'Travel', icon: 'map' },
];
@Component({
selector: 'app-sidebar-menu-skeleton-demo',
host: { class: 'flex w-full flex-col items-start gap-3' },
imports: [...WDS_SIDEBAR, WdsButtonComponent, Icon],
template: `
<wds-button variant="outline" size="sm" (click)="load()">Reload</wds-button>
<wds-sidebar-provider class="h-[320px] min-h-0 rounded-xl border border-border" shortcut="">
<wds-sidebar collapsible="none">
<wds-sidebar-content>
<wds-sidebar-group>
<wds-sidebar-group-label>Projects</wds-sidebar-group-label>
<wds-sidebar-menu [attr.aria-busy]="loading() ? 'true' : null">
@if (loading()) {
@for (i of [1, 2, 3, 4, 5]; track i) {
<wds-sidebar-menu-item><wds-sidebar-menu-skeleton show-icon /></wds-sidebar-menu-item>
}
} @else {
@for (project of projects; track project.name) {
<wds-sidebar-menu-item>
<wds-sidebar-menu-button><svg [appIcon]="project.icon" /><span>{{ project.name }}</span></wds-sidebar-menu-button>
</wds-sidebar-menu-item>
}
}
</wds-sidebar-menu>
</wds-sidebar-group>
</wds-sidebar-content>
</wds-sidebar>
<wds-sidebar-inset><div class="m-4 flex-1 rounded-xl bg-muted"></div></wds-sidebar-inset>
</wds-sidebar-provider>
`,
})
export class SidebarMenuSkeletonDemo {
protected readonly projects = PROJECTS;
protected readonly loading = signal(true);
constructor() {
this.load();
}
/** Stands in for a request that takes 1.5 s. */
protected load(): void {
this.loading.set(true);
setTimeout(() => this.loading.set(false), 1500);
}
}import { Component, computed, input } from '@angular/core';
import { ICONS } from './icons';
/** `<svg appIcon="house" />`: a 16×16 lucide icon, hidden from screen readers. */
@Component({
selector: 'svg[appIcon]',
host: {
viewBox: '0 0 24 24',
width: '16',
height: '16',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'aria-hidden': 'true',
},
template: `@for (d of paths(); track $index) {<svg:path [attr.d]="d" />}`,
})
export class Icon {
readonly appIcon = input.required<string>();
protected readonly paths = computed(() => ICONS[this.appIcon()] ?? []);
}/** Lucide icons of the sidebar examples: the `d` of each `<path>` (viewBox 0 0 24 24). */
export const ICONS: Record<string, string[]> = {
galleryVerticalEnd: ['M7 2h10', 'M5 6h14', 'M5 10h14a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2z'],
audioWaveform: ['M2 13a2 2 0 0 0 2-2V7a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0V4a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0v-4a2 2 0 0 1 2-2'],
command: ['M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3'],
chevronsUpDown: ['m7 15 5 5 5-5', 'm7 9 5-5 5 5'],
chevronRight: ['m9 18 6-6-6-6'],
chevronDown: ['m6 9 6 6 6-6'],
squareTerminal: ['m7 11 2-2-2-2', 'M11 13h4', 'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z'],
bot: ['M12 8V4H8', 'M6 8h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2z', 'M2 14h2', 'M20 14h2', 'M15 13v2', 'M9 13v2'],
bookOpen: ['M12 7v14', 'M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z'],
settings: ['M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z', 'M12 9a3 3 0 1 0 0 6a3 3 0 1 0 0 -6'],
frame: ['M22 6H2', 'M22 18H2', 'M6 2v20', 'M18 2v20'],
pieChart: ['M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z', 'M21.21 15.89A10 10 0 1 1 8 2.83'],
map: ['M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z', 'M15 5.764v15', 'M9 3.236v15'],
moreHorizontal: ['M12 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2', 'M19 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2', 'M5 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2'],
plus: ['M5 12h14', 'M12 5v14'],
folder: ['M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z'],
forward: ['m15 17 5-5-5-5', 'M4 18v-2a4 4 0 0 1 4-4h12'],
trash: ['M3 6h18', 'M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6', 'M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2', 'M10 11v6', 'M14 11v6'],
user: ['M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2', 'M12 3a4 4 0 1 0 0 8a4 4 0 1 0 0 -8'],
creditCard: ['M4 5h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z', 'M2 10h20'],
bell: ['M10.268 21a2 2 0 0 0 3.464 0', 'M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326'],
logOut: ['m16 17 5-5-5-5', 'M21 12H9', 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'],
house: ['M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8', 'M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z'],
inbox: ['M22 12h-6l-2 3h-4l-2-3H2', 'M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z'],
calendar: ['M8 2v4', 'M16 2v4', 'M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z', 'M3 10h18'],
search: ['m21 21-4.34-4.34', 'M11 3a8 8 0 1 0 0 16a8 8 0 1 0 0 -16'],
building: ['M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z', 'M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2', 'M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2', 'M10 6h4', 'M10 10h4', 'M10 14h4', 'M10 18h4'],
users: ['M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2', 'M9 3a4 4 0 1 0 0 8a4 4 0 1 0 0 -8', 'M22 21v-2a4 4 0 0 0-3-3.87', 'M16 3.13a4 4 0 0 1 0 7.75'],
server: ['M4 2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z', 'M4 14h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2z', 'M6 6h.01', 'M6 18h.01'],
list: ['M3 12h.01', 'M3 18h.01', 'M3 6h.01', 'M8 12h13', 'M8 18h13', 'M8 6h13'],
send: ['m22 2-7 20-4-9-9-4Z', 'M22 2 11 13'],
key: ['m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4', 'm21 2-9.6 9.6', 'M7.5 10a5.5 5.5 0 1 0 0 11a5.5 5.5 0 1 0 0 -11'],
fileText: ['M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z', 'M14 2v4a2 2 0 0 0 2 2h4', 'M10 9H8', 'M16 13H8', 'M16 17H8'],
shield: ['M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z'],
layoutGrid: ['M4 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z', 'M15 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z', 'M15 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z', 'M4 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z'],
lifeBuoy: ['M12 2a10 10 0 1 0 0 20a10 10 0 1 0 0 -20', 'M12 8a4 4 0 1 0 0 8a4 4 0 1 0 0 -8', 'm4.93 4.93 4.24 4.24', 'm14.83 9.17 4.24-4.24', 'm14.83 14.83 4.24 4.24', 'm9.17 14.83-4.24 4.24'],
};Second panel#
wds-sidebar elements sit side by side. The first one with collapsible="icon" is a fixed bar of sections with tooltips, the second one lists the pages of the chosen section. The outer sidebar has collapsible="icon", so when collapsed only the bar stays, and a click on a section opens the panel again.
import { Component, signal } from '@angular/core';
import { WDS_BREADCRUMB } from '@wds/angular/breadcrumb';
import { WDS_SIDEBAR } from '@wds/angular/sidebar';
import { Icon } from './icon';
interface Section {
title: string;
icon: string;
groups: { label: string; pages: { title: string; icon: string }[] }[];
}
const SECTIONS: Section[] = [
{
title: 'Organizations',
icon: 'building',
groups: [{ label: 'Organizations', pages: [{ title: 'All', icon: 'list' }, { title: 'New organization', icon: 'plus' }] }],
},
{
title: 'Users',
icon: 'users',
groups: [
{ label: 'Users', pages: [{ title: 'List', icon: 'list' }, { title: 'Invitations', icon: 'send' }] },
{ label: 'Activity', pages: [{ title: 'Sign-ins', icon: 'key' }, { title: 'Events', icon: 'fileText' }] },
],
},
{
title: 'Systems',
icon: 'server',
groups: [{ label: 'Systems', pages: [{ title: 'Servers', icon: 'server' }, { title: 'Monitoring', icon: 'pieChart' }] }],
},
];
const SETTINGS: Section = {
title: 'Settings',
icon: 'settings',
groups: [
{
label: 'General',
pages: [
{ title: 'General', icon: 'shield' },
{ title: 'Appearance', icon: 'layoutGrid' },
{ title: 'Notifications', icon: 'bell' },
],
},
{
label: 'Access and identity',
pages: [
{ title: 'Groups', icon: 'users' },
{ title: 'Roles', icon: 'user' },
{ title: 'API tokens', icon: 'key' },
],
},
],
};
@Component({
selector: 'app-sidebar-second-panel-demo',
host: { class: 'block w-full' },
imports: [...WDS_SIDEBAR, ...WDS_BREADCRUMB, Icon],
template: `
<wds-sidebar-provider
class="h-[600px] min-h-0 rounded-xl border border-border [--wds-sidebar-width:20rem]"
shortcut=""
[open]="open()"
(wdsOpenChange)="open.set($event.open)"
>
<!-- outer sidebar: collapsed, it leaves only the icon bar -->
<wds-sidebar collapsible="icon">
<!-- icon bar: always icons with tooltips -->
<wds-sidebar collapsible="icon">
<wds-sidebar-header>
<wds-sidebar-menu>
<wds-sidebar-menu-item>
<wds-sidebar-menu-button size="lg" tooltip="Admin">
<span class="flex size-8 flex-none items-center justify-center rounded-lg bg-sidebar-primary text-sm font-semibold text-sidebar-primary-foreground">A</span>
<span>Admin</span>
</wds-sidebar-menu-button>
</wds-sidebar-menu-item>
</wds-sidebar-menu>
</wds-sidebar-header>
<wds-sidebar-content>
<wds-sidebar-group>
<wds-sidebar-menu>
@for (s of sections; track s.title) {
<wds-sidebar-menu-item>
<wds-sidebar-menu-button [tooltip]="s.title" [active]="section() === s" (click)="openSection(s)">
<svg [appIcon]="s.icon" /><span>{{ s.title }}</span>
</wds-sidebar-menu-button>
</wds-sidebar-menu-item>
}
</wds-sidebar-menu>
</wds-sidebar-group>
</wds-sidebar-content>
<wds-sidebar-footer>
<wds-sidebar-menu>
<wds-sidebar-menu-item>
<wds-sidebar-menu-button [tooltip]="settings.title" [active]="section() === settings" (click)="openSection(settings)">
<svg [appIcon]="settings.icon" /><span>{{ settings.title }}</span>
</wds-sidebar-menu-button>
</wds-sidebar-menu-item>
</wds-sidebar-menu>
</wds-sidebar-footer>
</wds-sidebar>
<!-- panel: pages of the active section -->
<wds-sidebar collapsible="none" [label]="section().title">
<!-- same height as the content header (h-14), so the bottom borders line up -->
<wds-sidebar-header class="box-border h-14 flex-none border-b border-sidebar-border [&::part(base)]:justify-center [&::part(base)]:px-4 [&::part(base)]:py-0">
<span class="text-base font-medium">{{ section().title }}</span>
</wds-sidebar-header>
<wds-sidebar-content>
@for (group of section().groups; track group.label) {
<wds-sidebar-group>
<wds-sidebar-group-label>{{ group.label }}</wds-sidebar-group-label>
<wds-sidebar-menu>
@for (p of group.pages; track p.title) {
<wds-sidebar-menu-item>
<wds-sidebar-menu-button [active]="page() === p.title" (click)="page.set(p.title)">
<svg [appIcon]="p.icon" /><span>{{ p.title }}</span>
</wds-sidebar-menu-button>
</wds-sidebar-menu-item>
}
</wds-sidebar-menu>
</wds-sidebar-group>
}
</wds-sidebar-content>
</wds-sidebar>
</wds-sidebar>
<wds-sidebar-inset>
<header class="box-border flex h-14 flex-none items-center gap-2 border-b border-border px-4">
<wds-sidebar-trigger class="-ml-1" />
<span class="mx-1 h-4 w-px bg-border"></span>
<wds-breadcrumb class="min-w-0" label="Admin breadcrumb" wrap="nowrap">
<wds-breadcrumb-item class="max-md:hidden">
<wds-breadcrumb-link (click)="openSection(section())">{{ section().title }}</wds-breadcrumb-link>
</wds-breadcrumb-item>
<wds-breadcrumb-item>
<wds-breadcrumb-page class="truncate">{{ page() }}</wds-breadcrumb-page>
</wds-breadcrumb-item>
</wds-breadcrumb>
</header>
<div class="flex-1 overflow-auto p-4"><div class="h-full min-h-40 rounded-xl bg-muted"></div></div>
</wds-sidebar-inset>
</wds-sidebar-provider>
`,
})
export class SidebarSecondPanelDemo {
protected readonly sections = SECTIONS;
protected readonly settings = SETTINGS;
protected readonly open = signal(true);
protected readonly section = signal(SETTINGS);
protected readonly page = signal(SETTINGS.groups[0].pages[0].title);
/** A click on a section shows its pages and opens the panel if it was collapsed. */
protected openSection(section: Section): void {
this.section.set(section);
this.page.set(section.groups[0].pages[0].title);
this.open.set(true);
}
}import { Component, computed, input } from '@angular/core';
import { ICONS } from './icons';
/** `<svg appIcon="house" />`: a 16×16 lucide icon, hidden from screen readers. */
@Component({
selector: 'svg[appIcon]',
host: {
viewBox: '0 0 24 24',
width: '16',
height: '16',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'aria-hidden': 'true',
},
template: `@for (d of paths(); track $index) {<svg:path [attr.d]="d" />}`,
})
export class Icon {
readonly appIcon = input.required<string>();
protected readonly paths = computed(() => ICONS[this.appIcon()] ?? []);
}/** Lucide icons of the sidebar examples: the `d` of each `<path>` (viewBox 0 0 24 24). */
export const ICONS: Record<string, string[]> = {
galleryVerticalEnd: ['M7 2h10', 'M5 6h14', 'M5 10h14a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2z'],
audioWaveform: ['M2 13a2 2 0 0 0 2-2V7a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0V4a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0v-4a2 2 0 0 1 2-2'],
command: ['M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3'],
chevronsUpDown: ['m7 15 5 5 5-5', 'm7 9 5-5 5 5'],
chevronRight: ['m9 18 6-6-6-6'],
chevronDown: ['m6 9 6 6 6-6'],
squareTerminal: ['m7 11 2-2-2-2', 'M11 13h4', 'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z'],
bot: ['M12 8V4H8', 'M6 8h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2z', 'M2 14h2', 'M20 14h2', 'M15 13v2', 'M9 13v2'],
bookOpen: ['M12 7v14', 'M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z'],
settings: ['M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z', 'M12 9a3 3 0 1 0 0 6a3 3 0 1 0 0 -6'],
frame: ['M22 6H2', 'M22 18H2', 'M6 2v20', 'M18 2v20'],
pieChart: ['M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z', 'M21.21 15.89A10 10 0 1 1 8 2.83'],
map: ['M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z', 'M15 5.764v15', 'M9 3.236v15'],
moreHorizontal: ['M12 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2', 'M19 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2', 'M5 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2'],
plus: ['M5 12h14', 'M12 5v14'],
folder: ['M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z'],
forward: ['m15 17 5-5-5-5', 'M4 18v-2a4 4 0 0 1 4-4h12'],
trash: ['M3 6h18', 'M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6', 'M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2', 'M10 11v6', 'M14 11v6'],
user: ['M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2', 'M12 3a4 4 0 1 0 0 8a4 4 0 1 0 0 -8'],
creditCard: ['M4 5h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z', 'M2 10h20'],
bell: ['M10.268 21a2 2 0 0 0 3.464 0', 'M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326'],
logOut: ['m16 17 5-5-5-5', 'M21 12H9', 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'],
house: ['M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8', 'M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z'],
inbox: ['M22 12h-6l-2 3h-4l-2-3H2', 'M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z'],
calendar: ['M8 2v4', 'M16 2v4', 'M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z', 'M3 10h18'],
search: ['m21 21-4.34-4.34', 'M11 3a8 8 0 1 0 0 16a8 8 0 1 0 0 -16'],
building: ['M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z', 'M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2', 'M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2', 'M10 6h4', 'M10 10h4', 'M10 14h4', 'M10 18h4'],
users: ['M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2', 'M9 3a4 4 0 1 0 0 8a4 4 0 1 0 0 -8', 'M22 21v-2a4 4 0 0 0-3-3.87', 'M16 3.13a4 4 0 0 1 0 7.75'],
server: ['M4 2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z', 'M4 14h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2z', 'M6 6h.01', 'M6 18h.01'],
list: ['M3 12h.01', 'M3 18h.01', 'M3 6h.01', 'M8 12h13', 'M8 18h13', 'M8 6h13'],
send: ['m22 2-7 20-4-9-9-4Z', 'M22 2 11 13'],
key: ['m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4', 'm21 2-9.6 9.6', 'M7.5 10a5.5 5.5 0 1 0 0 11a5.5 5.5 0 1 0 0 -11'],
fileText: ['M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z', 'M14 2v4a2 2 0 0 0 2 2h4', 'M10 9H8', 'M16 13H8', 'M16 17H8'],
shield: ['M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z'],
layoutGrid: ['M4 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z', 'M15 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z', 'M15 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z', 'M4 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z'],
lifeBuoy: ['M12 2a10 10 0 1 0 0 20a10 10 0 1 0 0 -20', 'M12 8a4 4 0 1 0 0 8a4 4 0 1 0 0 -8', 'm4.93 4.93 4.24 4.24', 'm14.83 9.17 4.24-4.24', 'm14.83 14.83 4.24 4.24', 'm9.17 14.83-4.24 4.24'],
};Controlled#
open and wds-open-change keep the state in the app, for example to remember it between visits; setting open does not fire the event. toggle() toggles it from code. --wds-sidebar-width, --wds-sidebar-width-icon and --wds-sidebar-width-mobile on the provider set the widths. shortcut="" turns off ⌘/Ctrl+B: on this page only the Default example has it.
import { Component, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_SIDEBAR } from '@wds/angular/sidebar';
import { Icon } from './icon';
const ITEMS = [
{ title: 'Home', icon: 'house' },
{ title: 'Inbox', icon: 'inbox' },
{ title: 'Calendar', icon: 'calendar' },
{ title: 'Settings', icon: 'settings' },
];
@Component({
selector: 'app-sidebar-controlled-demo',
host: { class: 'flex w-full flex-col items-start gap-3' },
imports: [...WDS_SIDEBAR, WdsButtonComponent, Icon],
template: `
<div class="flex items-center gap-3">
<wds-button variant="outline" size="sm" (click)="open.set(!open())">{{ open() ? 'Collapse' : 'Expand' }}</wds-button>
<span class="text-sm text-muted-foreground">open: {{ open() }}</span>
</div>
<wds-sidebar-provider
class="h-[320px] min-h-0 rounded-xl border border-border [--wds-sidebar-width:20rem]"
shortcut=""
[open]="open()"
(wdsOpenChange)="open.set($event.open)"
>
<wds-sidebar variant="floating">
<wds-sidebar-content>
<wds-sidebar-group>
<wds-sidebar-group-label>Width 20rem</wds-sidebar-group-label>
<wds-sidebar-menu>
@for (item of items; track item.title) {
<wds-sidebar-menu-item>
<wds-sidebar-menu-button><svg [appIcon]="item.icon" /><span>{{ item.title }}</span></wds-sidebar-menu-button>
</wds-sidebar-menu-item>
}
</wds-sidebar-menu>
</wds-sidebar-group>
</wds-sidebar-content>
</wds-sidebar>
<wds-sidebar-inset>
<header class="flex h-12 flex-none items-center gap-2 px-3"><wds-sidebar-trigger /></header>
<div class="flex-1 px-4 pb-4"><div class="h-full rounded-xl bg-muted"></div></div>
</wds-sidebar-inset>
</wds-sidebar-provider>
`,
})
export class SidebarControlledDemo {
protected readonly items = ITEMS;
protected readonly open = signal(true);
}import { Component, computed, input } from '@angular/core';
import { ICONS } from './icons';
/** `<svg appIcon="house" />`: a 16×16 lucide icon, hidden from screen readers. */
@Component({
selector: 'svg[appIcon]',
host: {
viewBox: '0 0 24 24',
width: '16',
height: '16',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'aria-hidden': 'true',
},
template: `@for (d of paths(); track $index) {<svg:path [attr.d]="d" />}`,
})
export class Icon {
readonly appIcon = input.required<string>();
protected readonly paths = computed(() => ICONS[this.appIcon()] ?? []);
}/** Lucide icons of the sidebar examples: the `d` of each `<path>` (viewBox 0 0 24 24). */
export const ICONS: Record<string, string[]> = {
galleryVerticalEnd: ['M7 2h10', 'M5 6h14', 'M5 10h14a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2z'],
audioWaveform: ['M2 13a2 2 0 0 0 2-2V7a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0V4a2 2 0 0 1 4 0v13a2 2 0 0 0 4 0v-4a2 2 0 0 1 2-2'],
command: ['M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3'],
chevronsUpDown: ['m7 15 5 5 5-5', 'm7 9 5-5 5 5'],
chevronRight: ['m9 18 6-6-6-6'],
chevronDown: ['m6 9 6 6 6-6'],
squareTerminal: ['m7 11 2-2-2-2', 'M11 13h4', 'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z'],
bot: ['M12 8V4H8', 'M6 8h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2z', 'M2 14h2', 'M20 14h2', 'M15 13v2', 'M9 13v2'],
bookOpen: ['M12 7v14', 'M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z'],
settings: ['M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z', 'M12 9a3 3 0 1 0 0 6a3 3 0 1 0 0 -6'],
frame: ['M22 6H2', 'M22 18H2', 'M6 2v20', 'M18 2v20'],
pieChart: ['M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z', 'M21.21 15.89A10 10 0 1 1 8 2.83'],
map: ['M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z', 'M15 5.764v15', 'M9 3.236v15'],
moreHorizontal: ['M12 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2', 'M19 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2', 'M5 11a1 1 0 1 0 0 2a1 1 0 1 0 0 -2'],
plus: ['M5 12h14', 'M12 5v14'],
folder: ['M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z'],
forward: ['m15 17 5-5-5-5', 'M4 18v-2a4 4 0 0 1 4-4h12'],
trash: ['M3 6h18', 'M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6', 'M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2', 'M10 11v6', 'M14 11v6'],
user: ['M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2', 'M12 3a4 4 0 1 0 0 8a4 4 0 1 0 0 -8'],
creditCard: ['M4 5h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z', 'M2 10h20'],
bell: ['M10.268 21a2 2 0 0 0 3.464 0', 'M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326'],
logOut: ['m16 17 5-5-5-5', 'M21 12H9', 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'],
house: ['M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8', 'M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z'],
inbox: ['M22 12h-6l-2 3h-4l-2-3H2', 'M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z'],
calendar: ['M8 2v4', 'M16 2v4', 'M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z', 'M3 10h18'],
search: ['m21 21-4.34-4.34', 'M11 3a8 8 0 1 0 0 16a8 8 0 1 0 0 -16'],
building: ['M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z', 'M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2', 'M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2', 'M10 6h4', 'M10 10h4', 'M10 14h4', 'M10 18h4'],
users: ['M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2', 'M9 3a4 4 0 1 0 0 8a4 4 0 1 0 0 -8', 'M22 21v-2a4 4 0 0 0-3-3.87', 'M16 3.13a4 4 0 0 1 0 7.75'],
server: ['M4 2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z', 'M4 14h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2z', 'M6 6h.01', 'M6 18h.01'],
list: ['M3 12h.01', 'M3 18h.01', 'M3 6h.01', 'M8 12h13', 'M8 18h13', 'M8 6h13'],
send: ['m22 2-7 20-4-9-9-4Z', 'M22 2 11 13'],
key: ['m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4', 'm21 2-9.6 9.6', 'M7.5 10a5.5 5.5 0 1 0 0 11a5.5 5.5 0 1 0 0 -11'],
fileText: ['M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z', 'M14 2v4a2 2 0 0 0 2 2h4', 'M10 9H8', 'M16 13H8', 'M16 17H8'],
shield: ['M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z'],
layoutGrid: ['M4 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z', 'M15 3h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z', 'M15 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z', 'M4 14h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1z'],
lifeBuoy: ['M12 2a10 10 0 1 0 0 20a10 10 0 1 0 0 -20', 'M12 8a4 4 0 1 0 0 8a4 4 0 1 0 0 -8', 'm4.93 4.93 4.24 4.24', 'm14.83 9.17 4.24-4.24', 'm14.83 14.83 4.24 4.24', 'm9.17 14.83-4.24 4.24'],
};API References#
wds-sidebar-provider#
open on desktop, open-mobile on phones),
handles the ⌘/Ctrl+B shortcut and passes the state to the wds-sidebar, wds-sidebar-inset and
wds-sidebar-trigger inside it.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| open | boolean | true | wds-open-change. |
| open-mobile.openMobile | boolean | false | |
| shortcut | string | 'b' |
Events
| Event | Description |
|---|---|
| wds-open-changeCustomEvent<WdsSidebarOpenChangeDetail> | toggle()). |
| wds-open-mobile-changeCustomEvent<WdsSidebarOpenChangeDetail> |
Methods
| Method | Description |
|---|---|
| toggle() |
Slots
| Slot | Description |
|---|---|
| (default) | wds-sidebar and wds-sidebar-inset (the page content). |
CSS custom properties
| Property | Description |
|---|---|
| --wds-sidebar-width | |
| --wds-sidebar-width-icon | |
| --wds-sidebar-width-mobile |
wds-sidebar#
wds-sidebar-provider:
on desktop it collapses (collapsible), on phones it opens as a panel over the content.
Nested wds-sidebar elements sit side by side (a sidebar with a second panel):
a nested collapsible="icon" is a fixed icon bar with tooltips, and the others fill the rest.
The outer collapsible="icon" leaves only the icon bar when collapsed.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| side | WdsSidebarSide | 'left' | left or right (a right sidebar goes after the content). |
| variant | WdsSidebarVariant | 'sidebar' | sidebar: at the edge; floating: a floating card; inset: the content as a card on the sidebar background. |
| collapsible | WdsSidebarCollapsible | 'offcanvas' | offcanvas (slides out), icon (icons only), none (always expanded). |
| label | string | 'Panel boczny' | aria-label of the dialog). |
Slots
| Slot | Description |
|---|---|
| (default) | wds-sidebar-header, wds-sidebar-content, wds-sidebar-footer, wds-sidebar-rail or nested wds-sidebar elements. |
CSS parts (::part)
| Part | Description |
|---|---|
| container | |
| inner | |
| sheet | <dialog> with the panel on phones. |
wds-sidebar-header#
Slots
| Slot | Description |
|---|---|
| (default) |
CSS parts (::part)
| Part | Description |
|---|---|
| base |
wds-sidebar-content#
wds-sidebar-group).
Slots
| Slot | Description |
|---|---|
| (default) |
wds-sidebar-footer#
Slots
| Slot | Description |
|---|---|
| (default) |
CSS parts (::part)
| Part | Description |
|---|---|
| base |
wds-sidebar-separator#
CSS parts (::part)
| Part | Description |
|---|---|
| base |
wds-sidebar-group#
wds-sidebar-menu).
Slots
| Slot | Description |
|---|---|
| (default) | wds-sidebar-group-label, wds-sidebar-group-action, wds-sidebar-group-content. |
CSS parts (::part)
| Part | Description |
|---|---|
| base |
wds-sidebar-group-label#
wds-collapsible-trigger it renders as a button that expands the group
(it gets aria-expanded from wds-collapsible). Hidden when the sidebar is collapsed to icons.
Slots
| Slot | Description |
|---|---|
| (default) | <svg> icon (such as a chevron). |
CSS parts (::part)
| Part | Description |
|---|---|
| label | <div> or <button>). |
wds-sidebar-group-action#
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| label | string | '' |
Slots
| Slot | Description |
|---|---|
| (default) | <svg> icon. |
CSS parts (::part)
| Part | Description |
|---|---|
| button | <button>. |
wds-sidebar-group-content#
Slots
| Slot | Description |
|---|---|
| (default) | wds-sidebar-menu. |
wds-sidebar-menu#
role="list").
Slots
| Slot | Description |
|---|---|
| (default) | wds-sidebar-menu-item. |
wds-sidebar-menu-item#
role="listitem"): a button, an action, a badge and a submenu.
Slots
| Slot | Description |
|---|---|
| (default) | wds-sidebar-menu-button, wds-sidebar-menu-action, wds-sidebar-menu-badge, wds-sidebar-menu-sub. |
wds-sidebar-menu-button#
<button>, or a native <a> with href.
When the sidebar is collapsed to icons, only the icon stays and tooltip shows the name (a wds-tooltip).
aria-expanded, aria-haspopup and aria-label on the host go to the native element,
so the button can be the trigger of wds-collapsible and wds-dropdown.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| active | boolean | false | aria-current="page". |
| size | WdsSidebarMenuButtonSize | 'default' | sm, default or lg (two lines of text, such as a name and an email). |
| variant | WdsSidebarMenuButtonVariant | 'default' | outline: with a background and a border. |
| disabled | boolean | false | |
| href | string | undefined | ||
| target | string | undefined | _blank), used with href. | |
| tooltip | string | '' |
Slots
| Slot | Description |
|---|---|
| (default) | <svg> icon and text (a <span> is truncated with an ellipsis). |
CSS parts (::part)
| Part | Description |
|---|---|
| button | <button> or <a>. |
| tooltip |
wds-sidebar-menu-action#
wds-dropdown).
Hidden when the sidebar is collapsed to icons.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| label | string | '' | |
| show-on-hover.showOnHover | boolean | false |
Slots
| Slot | Description |
|---|---|
| (default) | <svg> icon. |
CSS parts (::part)
| Part | Description |
|---|---|
| button | <button>. |
wds-sidebar-menu-badge#
Slots
| Slot | Description |
|---|---|
| (default) |
CSS parts (::part)
| Part | Description |
|---|---|
| base |
wds-sidebar-menu-skeleton#
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| show-icon.showIcon | boolean | false |
CSS parts (::part)
| Part | Description |
|---|---|
| base |
wds-sidebar-menu-sub#
role="list") with a line on the left. Hidden when the sidebar is collapsed to icons.
Slots
| Slot | Description |
|---|---|
| (default) | wds-sidebar-menu-sub-item. |
CSS parts (::part)
| Part | Description |
|---|---|
| base |
wds-sidebar-menu-sub-item#
role="listitem").
Slots
| Slot | Description |
|---|---|
| (default) | wds-sidebar-menu-sub-button. |
wds-sidebar-menu-sub-button#
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| active | boolean | false | aria-current="page". |
| size | WdsSidebarMenuSubButtonSize | 'md' | sm or md. |
| disabled | boolean | false | |
| href | string | undefined | ||
| target | string | undefined | _blank), used with href. |
Slots
| Slot | Description |
|---|---|
| (default) | <svg> icon). |
CSS parts (::part)
| Part | Description |
|---|---|
| button | <a> or <button>. |
wds-sidebar-trigger#
wds-sidebar-provider (like ⌘/Ctrl+B).
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| label | string | 'Przełącz panel boczny' |
Slots
| Slot | Description |
|---|---|
| (default) |
CSS parts (::part)
| Part | Description |
|---|---|
| button | <button>. |
wds-sidebar-rail#
wds-sidebar.
Not in the Tab order (keyboard users have the trigger and the shortcut).
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| label | string | 'Przełącz panel boczny' |
wds-sidebar-inset#
wds-sidebar variant="inset" it becomes a card on the sidebar background.
Slots
| Slot | Description |
|---|---|
| (default) | wds-sidebar-trigger, the page). |
CSS parts (::part)
| Part | Description |
|---|---|
| base | variant="inset" a card with a margin and a shadow. |
Accessibility#
Screen reader#
wds-sidebar has no landmark role. When it holds the main navigation of the app, put the menus in a <nav> with an aria-label.
wds-sidebar-menu and wds-sidebar-menu-sub are lists (role="list") and their items list items. wds-sidebar-menu-button and wds-sidebar-menu-sub-button render a native <button>, or an <a> with href; active on a link sets aria-current="page".
tooltip is only visual (aria-hidden).
wds-sidebar-trigger has aria-expanded and the name from label (default Przełącz panel boczny, in Polish). wds-sidebar-rail is not in the Tab order. Icon-only buttons (wds-sidebar-group-action, wds-sidebar-menu-action) need a label.
<dialog> named by label: the rest of the page is inert and focus stays inside. wds-sidebar-menu-skeleton is hidden from screen readers; set aria-busy on the menu while it loads.
Keyboard support#
| Key | Function |
|---|---|
shortcut changes the key; an empty string turns it off). | |