Feedback
Sheet
Usage#
import { WdsSheetComponent } from '@wds/angular/sheet';Examples#
Default#
show() and close() open and close the sheet; heading and description name and describe it. The sheet slides in from the right by default. Content scrolls between the header and the footer, and buttons in the footer slot are stacked.
import { Component } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsInputComponent } from '@wds/angular/input';
import { WdsSheetComponent } from '@wds/angular/sheet';
@Component({
selector: 'app-sheet-default-demo',
imports: [WdsButtonComponent, ...WDS_FIELD, WdsInputComponent, WdsSheetComponent],
template: `
<wds-button variant="outline" (click)="sheet.show()">Open</wds-button>
<wds-sheet #sheet heading="Edit profile" description="Make changes to your profile here. Click save when you're done.">
<wds-field-group>
<wds-field>
<wds-field-label>Name</wds-field-label>
<wds-input value="Pedro Duarte"></wds-input>
</wds-field>
<wds-field>
<wds-field-label>Username</wds-field-label>
<wds-input value="@peduarte"></wds-input>
</wds-field>
</wds-field-group>
<wds-button slot="footer" (click)="sheet.close()">Save changes</wds-button>
<wds-button slot="footer" variant="outline" (click)="sheet.close()">Close</wds-button>
</wds-sheet>
`,
})
export class SheetDefaultDemo {}Side#
side sets the edge of the screen the sheet slides in from: top, right (default), bottom or left.
This sheet slides in from the right edge of the screen.
import { Component, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsSheetComponent } from '@wds/angular/sheet';
import type { WdsSheetSide } from '@wds/core/sheet';
@Component({
selector: 'app-sheet-side-demo',
imports: [WdsButtonComponent, WdsSheetComponent],
template: `
<div class="flex flex-wrap justify-center gap-2">
@for (s of sides; track s) {
<wds-button variant="outline" class="capitalize" (click)="side.set(s); sheet.show()">{{ s }}</wds-button>
}
</div>
<wds-sheet #sheet [side]="side()" heading="Edit profile" description="Make changes to your profile here. Click save when you're done.">
<p class="m-0 text-sm text-muted-foreground">This sheet slides in from the {{ side() }} edge of the screen.</p>
<wds-button slot="footer" (click)="sheet.close()">Save changes</wds-button>
<wds-button slot="footer" variant="outline" (click)="sheet.close()">Cancel</wds-button>
</wds-sheet>
`,
})
export class SheetSideDemo {
protected readonly sides: WdsSheetSide[] = ['top', 'right', 'bottom', 'left'];
protected readonly side = signal<WdsSheetSide>('right');
}No close button#
hide-close removes the close button from the header. Escape and a click on the backdrop still close the sheet; keep a button in the footer as well.
import { Component } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsSheetComponent } from '@wds/angular/sheet';
@Component({
selector: 'app-sheet-no-close-button-demo',
imports: [WdsButtonComponent, WdsSheetComponent],
template: `
<wds-button variant="outline" (click)="sheet.show()">Open sheet</wds-button>
<wds-sheet #sheet hide-close heading="No close button" description="This sheet doesn't have a close button in the top-right corner. Press Escape or click outside to close it.">
<wds-button slot="footer" variant="outline" (click)="sheet.close()">Done</wds-button>
</wds-sheet>
`,
})
export class SheetNoCloseButtonDemo {}Size#
--wds-sheet-size sets the maximum width of a left or right sheet (default 24rem; narrower screens get 75% of the width), or the height of a top or bottom one.
import { Component } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsSheetComponent } from '@wds/angular/sheet';
@Component({
selector: 'app-sheet-size-demo',
imports: [WdsButtonComponent, WdsSheetComponent],
template: `
<div class="flex flex-wrap justify-center gap-2">
<wds-button variant="outline" (click)="narrow.show()">20rem</wds-button>
<wds-button variant="outline" (click)="wide.show()">40rem</wds-button>
</div>
<wds-sheet #narrow class="[--wds-sheet-size:20rem]" heading="Narrow sheet" description="--wds-sheet-size: 20rem">
<wds-button slot="footer" variant="outline" (click)="narrow.close()">Close</wds-button>
</wds-sheet>
<wds-sheet #wide class="[--wds-sheet-size:40rem]" heading="Wide sheet" description="--wds-sheet-size: 40rem">
<wds-button slot="footer" variant="outline" (click)="wide.close()">Close</wds-button>
</wds-sheet>
`,
})
export class SheetSizeDemo {}Contained#
contained places the sheet inside the nearest positioned ancestor (position: relative) instead of the screen, e.g. as an inspector next to a list or a canvas. It is non-modal: no backdrop, no focus trap, and the content behind stays usable; Escape and the close button still close it. The modal property set to false alone gives a non-modal sheet at the edge of the screen.
import { Component, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsSheetComponent } from '@wds/angular/sheet';
interface Task {
id: string;
title: string;
owner: string;
due: string;
}
const TASKS: Task[] = [
{ id: 'T-101', title: 'Update the onboarding emails', owner: 'Olivia Martin', due: 'Oct 6' },
{ id: 'T-102', title: 'Review the pricing page', owner: 'Jackson Lee', due: 'Oct 8' },
{ id: 'T-103', title: 'Prepare the Q4 roadmap', owner: 'Isabella Nguyen', due: 'Oct 14' },
];
@Component({
selector: 'app-sheet-contained-demo',
host: { class: 'flex w-full justify-center' },
imports: [WdsButtonComponent, WdsSheetComponent],
template: `
<!-- position: relative makes this box the frame of the sheet. -->
<div class="relative h-80 w-full max-w-xl overflow-hidden rounded-xl border border-border bg-muted/30">
<ul class="m-0 flex list-none flex-col gap-2 p-3">
@for (task of tasks; track task.id) {
<li>
<button
type="button"
class="w-full cursor-pointer rounded-lg border border-border bg-background px-3 py-2 text-left text-sm hover:bg-accent"
[attr.aria-pressed]="selected()?.id === task.id"
(click)="selected.set(task)"
>
{{ task.title }}
</button>
</li>
}
</ul>
<wds-sheet contained inset class="[--wds-sheet-size:16rem]" [open]="!!selected()" [heading]="selected()?.title ?? ''" (wdsClose)="selected.set(null)">
@if (selected(); as task) {
<dl class="m-0 grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm">
<dt class="text-muted-foreground">ID</dt>
<dd class="m-0">{{ task.id }}</dd>
<dt class="text-muted-foreground">Owner</dt>
<dd class="m-0">{{ task.owner }}</dd>
<dt class="text-muted-foreground">Due</dt>
<dd class="m-0">{{ task.due }}</dd>
</dl>
}
<wds-button slot="footer" variant="outline" (click)="selected.set(null)">Close</wds-button>
</wds-sheet>
</div>
`,
})
export class SheetContainedDemo {
protected readonly tasks = TASKS;
protected readonly selected = signal<Task | null>(null);
}View customizer#
inset floats the sheet away from the screen edges with rounded corners, and divided separates the header and footer with borders. Inside: switches in horizontal wds-fields and a layout picker built from wds-radio cards. Wrap the footer buttons in an element to lay them out in a row.
import { Component, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WDS_RADIO } from '@wds/angular/radio';
import { WdsSheetComponent } from '@wds/angular/sheet';
import { WdsSwitchComponent } from '@wds/angular/switch';
type Toggle = 'autoSave' | 'smartFilters' | 'groupTotals' | 'pinnedColumns';
const TOGGLES: { id: Toggle; label: string; description: string }[] = [
{ id: 'autoSave', label: 'Auto save', description: 'Save view changes automatically.' },
{ id: 'smartFilters', label: 'Smart filters', description: 'Suggest useful filters as you work.' },
{ id: 'groupTotals', label: 'Group totals', description: 'Show counts inside grouped sections.' },
{ id: 'pinnedColumns', label: 'Pinned columns', description: 'Keep priority columns visible while scrolling.' },
];
const DEFAULTS: Record<Toggle, boolean> = { autoSave: true, smartFilters: true, groupTotals: false, pinnedColumns: true };
@Component({
selector: 'app-sheet-view-customizer-demo',
imports: [WdsButtonComponent, ...WDS_FIELD, ...WDS_RADIO, WdsSheetComponent, WdsSwitchComponent],
template: `
<wds-button variant="outline" (click)="sheet.show()">Customize view</wds-button>
<wds-sheet #sheet inset divided heading="Customize view">
<wds-field-group class="gap-4">
@for (toggle of toggles; track toggle.id) {
<wds-field orientation="horizontal">
<wds-field-content>
<wds-field-label>{{ toggle.label }}</wds-field-label>
<wds-field-description>{{ toggle.description }}</wds-field-description>
</wds-field-content>
<wds-switch [checked]="view()[toggle.id]" (wdsChange)="set(toggle.id, $event.checked)"></wds-switch>
</wds-field>
<wds-field-separator></wds-field-separator>
}
<wds-field-set>
<wds-field-legend>Layout mode</wds-field-legend>
<wds-field-description>Choose how the workspace is framed.</wds-field-description>
<wds-radio-group class="grid grid-cols-2 gap-3" [value]="layout()" (wdsChange)="layout.set($event.value)">
<wds-radio variant="card" value="deck">
<svg class="mb-2 block w-full rounded-md bg-muted" viewBox="0 0 120 64" aria-hidden="true">
<rect x="8" y="8" width="22" height="48" rx="3" class="fill-muted-foreground/25" />
<rect x="36" y="8" width="76" height="10" rx="3" class="fill-muted-foreground/40" />
<rect x="36" y="24" width="76" height="32" rx="3" class="fill-muted-foreground/15" />
</svg>
Command deck
</wds-radio>
<wds-radio variant="card" value="rail">
<svg class="mb-2 block w-full rounded-md bg-muted" viewBox="0 0 120 64" aria-hidden="true">
<rect x="8" y="8" width="76" height="10" rx="3" class="fill-muted-foreground/40" />
<rect x="8" y="24" width="76" height="32" rx="3" class="fill-muted-foreground/15" />
<rect x="90" y="8" width="22" height="48" rx="3" class="fill-muted-foreground/25" />
</svg>
Insight rail
</wds-radio>
</wds-radio-group>
</wds-field-set>
</wds-field-group>
<div slot="footer" class="grid grid-cols-2 gap-2">
<wds-button variant="outline" (click)="reset()">Reset</wds-button>
<wds-button (click)="sheet.close()">Save</wds-button>
</div>
</wds-sheet>
`,
})
export class SheetViewCustomizerDemo {
protected readonly toggles = TOGGLES;
protected readonly view = signal({ ...DEFAULTS });
protected readonly layout = signal('deck');
protected set(id: Toggle, checked: boolean): void {
this.view.update((view) => ({ ...view, [id]: checked }));
}
protected reset(): void {
this.view.set({ ...DEFAULTS });
this.layout.set('deck');
}
}Settings form#
wds-field-set and wds-field-legend) and horizontal fields. The content scrolls while the header and the footer stay in place. Reset calls reset() on the <form>: every wds control returns to its initial value.
import { Component } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsInputComponent } from '@wds/angular/input';
import { WDS_SELECT } from '@wds/angular/select';
import { WdsSheetComponent } from '@wds/angular/sheet';
import { WdsSwitchComponent } from '@wds/angular/switch';
@Component({
selector: 'app-sheet-settings-form-demo',
imports: [WdsButtonComponent, ...WDS_FIELD, WdsInputComponent, ...WDS_SELECT, WdsSheetComponent, WdsSwitchComponent],
template: `
<wds-button variant="outline" (click)="sheet.show()">Relay settings</wds-button>
<wds-sheet #sheet inset divided class="[--wds-sheet-size:28rem]" heading="Notification relay" description="Where and when the relay delivers alerts.">
<form #form class="flex flex-col gap-6">
<wds-field-set>
<wds-field-legend>Delivery</wds-field-legend>
<wds-field-group class="gap-4">
<wds-field orientation="horizontal">
<wds-field-label>Panel label</wds-field-label>
<wds-input class="w-52" name="label" value="Launch desk relay"></wds-input>
</wds-field>
<div class="flex items-center gap-3">
<span id="quiet-window" class="text-sm font-medium">Quiet window</span>
<div class="ms-auto flex w-52 items-center gap-2" role="group" aria-labelledby="quiet-window">
<wds-select class="min-w-0 flex-1" name="quietFrom" value="18:30" aria-label="From">
<wds-select-item value="17:00">17:00</wds-select-item>
<wds-select-item value="18:00">18:00</wds-select-item>
<wds-select-item value="18:30">18:30</wds-select-item>
<wds-select-item value="20:00">20:00</wds-select-item>
</wds-select>
<span class="text-sm text-muted-foreground">to</span>
<wds-select class="min-w-0 flex-1" name="quietTo" value="08:00" aria-label="To">
<wds-select-item value="06:00">06:00</wds-select-item>
<wds-select-item value="07:00">07:00</wds-select-item>
<wds-select-item value="08:00">08:00</wds-select-item>
<wds-select-item value="09:00">09:00</wds-select-item>
</wds-select>
</div>
</div>
<wds-field orientation="horizontal">
<wds-field-label>Region clock</wds-field-label>
<wds-select class="w-52" name="timeZone" value="Europe/Warsaw">
<wds-select-item value="Europe/London">Europe/London</wds-select-item>
<wds-select-item value="Europe/Warsaw">Europe/Warsaw</wds-select-item>
<wds-select-item value="America/New_York">America/New_York</wds-select-item>
<wds-select-item value="Asia/Tokyo">Asia/Tokyo</wds-select-item>
</wds-select>
</wds-field>
<wds-field orientation="horizontal">
<wds-field-label>Delivery channel</wds-field-label>
<wds-select class="w-52" name="channel" value="inbox">
<wds-select-item value="inbox">Workspace inbox</wds-select-item>
<wds-select-item value="email">Email</wds-select-item>
<wds-select-item value="chat">Team chat</wds-select-item>
</wds-select>
</wds-field>
<wds-field orientation="horizontal">
<wds-field-label>Digest cadence</wds-field-label>
<wds-select class="w-52" name="cadence" value="twice-daily">
<wds-select-item value="hourly">Hourly</wds-select-item>
<wds-select-item value="twice-daily">Twice daily</wds-select-item>
<wds-select-item value="daily">Daily</wds-select-item>
</wds-select>
</wds-field>
<wds-field orientation="horizontal">
<wds-field-label>Workdays only</wds-field-label>
<wds-switch class="w-52" name="workdays" checked></wds-switch>
</wds-field>
</wds-field-group>
</wds-field-set>
<wds-field-set>
<wds-field-legend>Routing</wds-field-legend>
<wds-field-group class="gap-4">
<wds-field orientation="horizontal">
<wds-field-label>Digest route</wds-field-label>
<wds-select class="w-52" name="route" value="ops">
<wds-select-item value="ops">Ops brief</wds-select-item>
<wds-select-item value="leadership">Leadership summary</wds-select-item>
<wds-select-item value="on-call">On-call channel</wds-select-item>
</wds-select>
</wds-field>
<wds-field orientation="horizontal">
<wds-field-label>Priority escalation</wds-field-label>
<wds-switch class="w-52" name="escalation" checked></wds-switch>
</wds-field>
<wds-field orientation="horizontal">
<wds-field-label>Escalation delay</wds-field-label>
<wds-select class="w-52" name="delay" value="10">
<wds-select-item value="5">5 minutes</wds-select-item>
<wds-select-item value="10">10 minutes</wds-select-item>
<wds-select-item value="30">30 minutes</wds-select-item>
</wds-select>
</wds-field>
<wds-field orientation="horizontal">
<wds-field-label>Retry failed sends</wds-field-label>
<wds-switch class="w-52" name="retry" checked></wds-switch>
</wds-field>
</wds-field-group>
</wds-field-set>
</form>
<div slot="footer" class="grid grid-cols-2 gap-2">
<wds-button variant="outline" (click)="form.reset()">Reset</wds-button>
<wds-button (click)="sheet.close()">Save settings</wds-button>
</div>
</wds-sheet>
`,
})
export class SheetSettingsFormDemo {}Inline editing#
wds-date-picker, wds-select, wds-input-group and wds-switch next to read-only badges. wds-change and wds-input bubble up to the <form>, so one listener enables Save; the button shows loading while the change is saved and a toast confirms it.
Quick tips
- Changes are kept until you save them.
- Escape or the backdrop closes the sheet without saving.
import { Component, signal } from '@angular/core';
import { WdsBadgeComponent } from '@wds/angular/badge';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsDatePickerComponent } from '@wds/angular/date-picker';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsInputComponent } from '@wds/angular/input';
import { WDS_INPUT_GROUP } from '@wds/angular/input-group';
import { WDS_SELECT } from '@wds/angular/select';
import { WdsSeparatorComponent } from '@wds/angular/separator';
import { WdsSheetComponent } from '@wds/angular/sheet';
import { WdsSwitchComponent } from '@wds/angular/switch';
import { toast } from '@wds/angular/toast';
@Component({
selector: 'app-sheet-inline-editing-demo',
imports: [
WdsBadgeComponent,
WdsButtonComponent,
WdsDatePickerComponent,
...WDS_FIELD,
WdsInputComponent,
...WDS_INPUT_GROUP,
...WDS_SELECT,
WdsSeparatorComponent,
WdsSheetComponent,
WdsSwitchComponent,
],
template: `
<wds-button variant="outline" (click)="sheet.show()">Open ticket</wds-button>
<wds-sheet #sheet inset divided class="[--wds-sheet-size:28rem]" heading="Ticket details" description="TCK-2048 · Workspace access request">
<form (wds-change)="dirty.set(true)" (wds-input)="dirty.set(true)" class="flex flex-col gap-3">
<wds-field orientation="horizontal">
<wds-field-label class="w-28 flex-none text-muted-foreground">Due date</wds-field-label>
<wds-date-picker class="min-w-0 flex-1" name="due" value="2026-10-30"></wds-date-picker>
</wds-field>
<wds-field orientation="horizontal">
<wds-field-label class="w-28 flex-none text-muted-foreground">Status</wds-field-label>
<wds-select class="min-w-0 flex-1" name="status" value="open">
<wds-select-item value="open">Open</wds-select-item>
<wds-select-item value="in-progress">In progress</wds-select-item>
<wds-select-item value="resolved">Resolved</wds-select-item>
</wds-select>
</wds-field>
<wds-field orientation="horizontal">
<wds-field-label class="w-28 flex-none text-muted-foreground">Priority</wds-field-label>
<wds-select class="min-w-0 flex-1" name="priority" value="medium">
<wds-select-item value="low">Low</wds-select-item>
<wds-select-item value="medium">Medium</wds-select-item>
<wds-select-item value="high">High</wds-select-item>
</wds-select>
</wds-field>
<wds-field orientation="horizontal">
<wds-field-label class="w-28 flex-none text-muted-foreground">Assignee</wds-field-label>
<wds-select class="min-w-0 flex-1" name="assignee" value="mira">
<wds-select-item value="mira">Mira Stone</wds-select-item>
<wds-select-item value="sana">Sana Qureshi</wds-select-item>
<wds-select-item value="none">Unassigned</wds-select-item>
</wds-select>
</wds-field>
<wds-field orientation="horizontal">
<wds-field-label class="w-28 flex-none text-muted-foreground">Channel</wds-field-label>
<wds-input-group class="min-w-0 flex-1">
<wds-input name="channel" value="identity-access"></wds-input>
<wds-input-group-addon><wds-input-group-text>#</wds-input-group-text></wds-input-group-addon>
</wds-input-group>
</wds-field>
<wds-field orientation="horizontal">
<wds-field-label class="w-28 flex-none text-muted-foreground">Notify requester</wds-field-label>
<wds-switch class="flex-1" name="notify" checked></wds-switch>
</wds-field>
<wds-field orientation="horizontal">
<wds-field-title class="w-28 flex-none text-muted-foreground">Tags</wds-field-title>
<div class="flex flex-1 flex-wrap gap-1.5">
<wds-badge variant="secondary">Feature</wds-badge>
<wds-badge variant="secondary">Security</wds-badge>
</div>
</wds-field>
<wds-field orientation="horizontal">
<wds-field-title class="w-28 flex-none text-muted-foreground">SLA</wds-field-title>
<div class="flex-1"><wds-badge variant="outline">Met · 3h left</wds-badge></div>
</wds-field>
</form>
<wds-separator class="mt-6 mb-4"></wds-separator>
<section aria-labelledby="ticket-tips">
<h3 id="ticket-tips" class="m-0 text-sm font-medium">Quick tips</h3>
<ul class="mt-2 mb-0 list-disc space-y-1 ps-5 text-sm text-muted-foreground">
<li>Changes are kept until you save them.</li>
<li>Escape or the backdrop closes the sheet without saving.</li>
</ul>
</section>
<div slot="footer" class="grid grid-cols-2 gap-2">
<wds-button [disabled]="!dirty()" [loading]="saving()" (click)="save()">Save changes</wds-button>
<wds-button variant="outline" (click)="sheet.close()">Close</wds-button>
</div>
</wds-sheet>
`,
})
export class SheetInlineEditingDemo {
protected readonly dirty = signal(false);
protected readonly saving = signal(false);
protected save(): void {
this.saving.set(true);
// A simulated request.
setTimeout(() => {
this.saving.set(false);
this.dirty.set(false);
toast.success('Ticket updated', { description: 'TCK-2048 has been saved.' });
}, 800);
}
}Metric details#
aria-labelledby). Without a footer, the close button and Escape close the sheet.
Definition
- Activation event
template_applied- Source feature
- Template gallery
- Segment
- Trial workspaces over 20 seats
- Window
- Last 28 days
- Refreshed
- Hourly · 2 h ago
Owners
Alert rule
Recent changes
import { Component } from '@angular/core';
import { WDS_AVATAR } from '@wds/angular/avatar';
import { WdsBadgeComponent } from '@wds/angular/badge';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsProgressComponent } from '@wds/angular/progress';
import { WdsSheetComponent } from '@wds/angular/sheet';
import { WdsTimelineComponent, WdsTimelineItemComponent } from '@wds/angular/timeline';
@Component({
selector: 'app-sheet-metric-details-demo',
imports: [...WDS_AVATAR, WdsBadgeComponent, WdsButtonComponent, WdsProgressComponent, WdsSheetComponent, WdsTimelineComponent, WdsTimelineItemComponent],
template: `
<wds-button variant="outline" (click)="sheet.show()">Open metric</wds-button>
<wds-sheet #sheet inset divided heading="Activation rate" description="MET-609 · 801 of 1,284 workspaces activated in 28 days.">
<div class="-m-4 divide-y divide-border text-sm">
<section class="p-4" aria-label="Current value">
<div class="flex flex-wrap items-center gap-2">
<span class="text-4xl font-semibold tracking-tight">62.4%</span>
<wds-badge variant="secondary">+3.1 pts</wds-badge>
<span class="ms-auto text-muted-foreground">vs 59.3% prior 28 days</span>
</div>
<wds-progress
class="mt-4 [--wds-progress-height:8px]"
value="62.4"
max="70"
label="Progress to target"
value-text="62.4% of the 70% target"
></wds-progress>
<div class="mt-2 flex justify-between text-muted-foreground">
<span>Target 70%</span>
<span>89% of target</span>
</div>
</section>
<section class="p-4" aria-labelledby="metric-definition">
<h3 id="metric-definition" class="m-0 mb-3 text-xs font-medium tracking-wide text-muted-foreground uppercase">Definition</h3>
<dl class="m-0 grid grid-cols-[8rem_1fr] gap-x-4 gap-y-2">
<dt class="text-muted-foreground">Activation event</dt>
<dd class="m-0"><code class="font-mono text-xs">template_applied</code></dd>
<dt class="text-muted-foreground">Source feature</dt>
<dd class="m-0">Template gallery</dd>
<dt class="text-muted-foreground">Segment</dt>
<dd class="m-0">Trial workspaces over 20 seats</dd>
<dt class="text-muted-foreground">Window</dt>
<dd class="m-0">Last 28 days</dd>
<dt class="text-muted-foreground">Refreshed</dt>
<dd class="m-0">Hourly · 2 h ago</dd>
</dl>
</section>
<section class="p-4" aria-labelledby="metric-owners">
<h3 id="metric-owners" class="m-0 mb-3 text-xs font-medium tracking-wide text-muted-foreground uppercase">Owners</h3>
<div class="flex items-center gap-3">
<wds-avatar-group class="[--wds-avatar-group-overlap:4px]">
<wds-avatar alt="Mira Stone">MS</wds-avatar>
<wds-avatar alt="Sana Qureshi">SQ</wds-avatar>
<wds-avatar alt="Ivo Vidal">IV</wds-avatar>
</wds-avatar-group>
<span class="ms-auto text-muted-foreground"><span class="font-medium text-foreground">Mira Stone</span> · lead</span>
</div>
</section>
<section class="p-4" aria-labelledby="metric-alert">
<h3 id="metric-alert" class="m-0 mb-3 text-xs font-medium tracking-wide text-muted-foreground uppercase">Alert rule</h3>
<div class="flex items-start gap-2">
<div class="flex-1">
<div class="font-medium">Activation drop</div>
<div class="text-muted-foreground">Fires below 55% · 2-day window · Sana Qureshi</div>
</div>
<wds-badge variant="outline">Armed</wds-badge>
</div>
</section>
<section class="p-4" aria-labelledby="metric-changes">
<h3 id="metric-changes" class="m-0 mb-3 text-xs font-medium tracking-wide text-muted-foreground uppercase">Recent changes</h3>
<wds-timeline value="1" aria-label="Recent changes">
<wds-timeline-item heading="Rollout raised to 50%">Sep 28 · Mira Stone</wds-timeline-item>
<wds-timeline-item heading="Alert threshold set to 55%">Sep 21 · Sana Qureshi</wds-timeline-item>
<wds-timeline-item heading="Segment narrowed to 20+ seats">Sep 14 · Ivo Vidal</wds-timeline-item>
</wds-timeline>
</section>
</div>
</wds-sheet>
`,
})
export class SheetMetricDetailsDemo {}Service#
WdsSheetService.open() creates the sheet from a component: data goes to the component through WDS_OVERLAY_DATA, side, inset, divided and size set the look, okText / cancelText add footer buttons, and the value returned by onOk comes out of closed. HTML and React have no service, so their tabs show the same sheet declared in the markup.
import { Component, inject, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WDS_RADIO } from '@wds/angular/radio';
import { WDS_OVERLAY_DATA, WdsSheetService } from '@wds/angular/sheet';
import { WdsSwitchComponent } from '@wds/angular/switch';
interface Filters {
status: string;
mine: boolean;
}
/** Content of the sheet: any component. */
@Component({
selector: 'app-filters-panel',
imports: [...WDS_FIELD, ...WDS_RADIO, WdsSwitchComponent],
template: `
<wds-field-group>
<wds-field-set>
<wds-field-legend>Status</wds-field-legend>
<wds-radio-group [value]="filters().status" (wdsChange)="filters.set({ status: $event.value, mine: filters().mine })">
<wds-radio value="all">All</wds-radio>
<wds-radio value="open">Open</wds-radio>
<wds-radio value="resolved">Resolved</wds-radio>
</wds-radio-group>
</wds-field-set>
<wds-switch [checked]="filters().mine" (wdsChange)="filters.set({ status: filters().status, mine: $event.checked })">
Only assigned to me
</wds-switch>
</wds-field-group>
`,
})
export class FiltersPanel {
// config.data of open()
readonly filters = signal(inject<Filters>(WDS_OVERLAY_DATA));
}
@Component({
selector: 'app-sheet-service-demo',
imports: [WdsButtonComponent],
template: `
<div class="flex flex-wrap items-center justify-center gap-3">
<wds-button variant="outline" (click)="openFilters()">Filters</wds-button>
<span class="text-sm text-muted-foreground">Status: {{ filters().status }}{{ filters().mine ? ' · assigned to me' : '' }}</span>
</div>
`,
})
export class SheetServiceDemo {
private readonly sheets = inject(WdsSheetService);
protected readonly filters = signal<Filters>({ status: 'all', mine: false });
protected openFilters(): void {
this.sheets
.open<FiltersPanel, Filters>({
title: 'Filters',
description: 'Narrow down the list of tickets.',
content: FiltersPanel,
data: this.filters(),
divided: true,
okText: 'Apply',
cancelText: 'Cancel',
// The value returned here is emitted by closed.
onOk: (panel) => panel?.filters(),
})
.closed.subscribe((value) => {
// undefined: closed with Cancel, Escape or the backdrop.
if (value !== undefined) this.filters.set(value);
});
}
}API References#
wds-sheet#
close(). The content scrolls between
a fixed header and footer. With modal=false the page behind stays usable (no backdrop, no focus trap);
contained also places the panel inside the nearest positioned ancestor instead of the screen.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| side | WdsSheetSide | 'right' | |
| inset | boolean | false | |
| divided | boolean | false | |
| modal | boolean | true | false: no backdrop and no focus trap, the content behind stays usable (e.g. an inspector next to a canvas). |
| contained | boolean | false | position: relative) instead of the screen.
Always non-modal, because a modal dialog lives in the top layer above the whole page. |
| open | boolean | false | show() and close(). |
| heading | string | '' | heading slot). |
| description | string | '' | description slot), linked as aria-describedby. |
| hide-close.hideClose | boolean | false | |
| static-backdrop.staticBackdrop | boolean | false | |
| dismissible | boolean | true | false blocks closing with Escape, the backdrop (and dragging in a drawer). |
| close-label.closeLabel | string | 'Zamknij' |
Events
| Event | Description |
|---|---|
| wds-closeCustomEvent<void> | close()), before the exit animation. |
| wds-after-closeCustomEvent<void> |
Methods
| Method | Description |
|---|---|
| show() | |
| close() |
Slots
| Slot | Description |
|---|---|
| (default) | autofocus gets focus when it opens (otherwise the first focusable element). |
| heading | heading attribute). |
| description | description attribute). |
| footer |
CSS parts (::part)
| Part | Description |
|---|---|
| dialog | <dialog> (the panel). |
| panel | |
| header | |
| title | |
| description | |
| close | |
| body | |
| footer |
CSS custom properties
| Property | Description |
|---|---|
| --wds-sheet-size | |
| --wds-sheet-backdrop |
Accessibility#
Screen reader#
<dialog> named by heading and described by description (aria-labelledby, aria-describedby). Always give it a heading.
contained or modal set to false the page stays usable and focus is not trapped.
close-label (default Zamknij, in Polish). With hide-close, keep another way to close the sheet, such as a button in the footer.
wds-field-set and wds-field-legend, so each section is announced with its name.
Keyboard support#
| Key | Function |
|---|---|
dismissible is false). |