Feedback
Drawer
Usage#
import { WdsDrawerComponent } from '@wds/angular/drawer';Examples#
Default#
show() and close() open and close the drawer; heading and description name and describe it. With handle, drag the handle or the header toward the edge (or flick it) to close. Buttons in the footer slot are stacked.
Drag the handle or the header down to close the drawer.
import { Component } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsDrawerComponent } from '@wds/angular/drawer';
@Component({
selector: 'app-drawer-default-demo',
imports: [WdsButtonComponent, WdsDrawerComponent],
template: `
<wds-button variant="outline" (click)="drawer.show()">Open drawer</wds-button>
<wds-drawer #drawer handle heading="Are you absolutely sure?" description="This action cannot be undone.">
<p class="m-0 text-sm text-muted-foreground">Drag the handle or the header down to close the drawer.</p>
<wds-button slot="footer" (click)="drawer.close()">Submit</wds-button>
<wds-button slot="footer" variant="outline" (click)="drawer.close()">Cancel</wds-button>
</wds-drawer>
`,
})
export class DrawerDefaultDemo {}Position#
placement sets the edge: top, right, bottom (default) or left.
Close it with Escape, a click on the backdrop or the button.
import { Component, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsDrawerComponent } from '@wds/angular/drawer';
import type { WdsDrawerPlacement } from '@wds/core/drawer';
@Component({
selector: 'app-drawer-position-demo',
imports: [WdsButtonComponent, WdsDrawerComponent],
template: `
<div class="flex flex-wrap justify-center gap-2">
@for (p of placements; track p) {
<wds-button variant="outline" (click)="placement.set(p); drawer.show()">{{ p }}</wds-button>
}
</div>
<wds-drawer #drawer [placement]="placement()" heading="Drawer" [description]="'Slides in from the ' + placement() + ' edge.'">
<p class="m-0 text-sm text-muted-foreground">Close it with Escape, a click on the backdrop or the button.</p>
<wds-button slot="footer" (click)="drawer.close()">Close</wds-button>
</wds-drawer>
`,
})
export class DrawerPositionDemo {
protected readonly placements: WdsDrawerPlacement[] = ['top', 'right', 'bottom', 'left'];
protected readonly placement = signal<WdsDrawerPlacement>('bottom');
}Size#
--wds-drawer-size sets the width of a left or right drawer, or the height of a top or bottom one.
Drawer content.
Drawer content.
import { Component } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsDrawerComponent } from '@wds/angular/drawer';
@Component({
selector: 'app-drawer-size-demo',
imports: [WdsButtonComponent, WdsDrawerComponent],
template: `
<div class="flex flex-wrap justify-center gap-2">
<wds-button variant="outline" (click)="small.show()">Small (20rem)</wds-button>
<wds-button variant="outline" (click)="large.show()">Large (40rem)</wds-button>
</div>
<wds-drawer #small placement="right" heading="Small drawer" description="20rem wide." class="[--wds-drawer-size:20rem]">
<p class="m-0 text-sm text-muted-foreground">Drawer content.</p>
</wds-drawer>
<!-- min() keeps a margin on narrow screens. -->
<wds-drawer #large placement="right" heading="Large drawer" description="40rem wide." class="[--wds-drawer-size:min(40rem,100vw_-_16px)]">
<p class="m-0 text-sm text-muted-foreground">Drawer content.</p>
</wds-drawer>
`,
})
export class DrawerSizeDemo {}Nested#
import { Component } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsDrawerComponent } from '@wds/angular/drawer';
@Component({
selector: 'app-drawer-nested-demo',
imports: [WdsButtonComponent, WdsDrawerComponent],
template: `
<wds-button variant="outline" (click)="outer.show()">Open drawer</wds-button>
<wds-drawer #outer handle heading="First level" description="Open the next drawer from here.">
<wds-button slot="footer" (click)="inner.show()">Open nested drawer</wds-button>
</wds-drawer>
<wds-drawer #inner handle heading="Second level" description="Escape closes only this drawer.">
<wds-button slot="footer" variant="outline" (click)="inner.close()">Back</wds-button>
</wds-drawer>
`,
})
export class DrawerNestedDemo {}Non-modal#
modal set to false (a property) opens the drawer without a backdrop: the page stays usable and focus is not trapped.
import { Component, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsDrawerComponent } from '@wds/angular/drawer';
@Component({
selector: 'app-drawer-non-modal-demo',
imports: [WdsButtonComponent, WdsDrawerComponent],
template: `
<div class="flex flex-wrap items-center justify-center gap-2">
<wds-button variant="outline" (click)="drawer.show()">Open drawer</wds-button>
<wds-button variant="ghost" (click)="clicks.set(clicks() + 1)">Clicked {{ clicks() }}×</wds-button>
</div>
<wds-drawer #drawer [modal]="false" placement="right" heading="Non-modal" description="The page below stays usable.">
<wds-button slot="footer" variant="outline" (click)="drawer.close()">Close</wds-button>
</wds-drawer>
`,
})
export class DrawerNonModalDemo {
protected readonly clicks = signal(0);
}Snap points#
snapPoints lets a top or bottom drawer stop at several heights (fractions of the screen, pixels or strings such as 320px). Dragging moves between them and fires wds-snap-change; snapPoint sets the active one.
Row 1
Row 2
Row 3
Row 4
Row 5
Row 6
Row 7
Row 8
Row 9
Row 10
Row 11
Row 12
Row 13
Row 14
Row 15
Row 16
Row 17
Row 18
Row 19
Row 20
Row 21
Row 22
Row 23
Row 24
Row 25
Row 26
Row 27
Row 28
Row 29
Row 30
import { Component, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsDrawerComponent } from '@wds/angular/drawer';
import type { WdsDrawerSnapPoint } from '@wds/core/drawer';
@Component({
selector: 'app-drawer-snap-points-demo',
imports: [WdsButtonComponent, WdsDrawerComponent],
template: `
<div class="flex flex-wrap items-center justify-center gap-3">
<wds-button variant="outline" (click)="drawer.show()">Open drawer</wds-button>
<span class="text-sm text-muted-foreground">Snap point: {{ snapPoint() }}</span>
</div>
<wds-drawer
#drawer
handle
heading="Snap points"
description="Drag up or down: 30%, 60% or full height."
[snapPoints]="snapPoints"
[snapPoint]="snapPoint()"
(wdsSnapChange)="snapPoint.set($event.snapPoint)"
>
@for (i of rows; track i) {
<p>Row {{ i }}</p>
}
</wds-drawer>
`,
})
export class DrawerSnapPointsDemo {
protected readonly snapPoints: WdsDrawerSnapPoint[] = [0.3, 0.6, 1];
protected readonly snapPoint = signal<WdsDrawerSnapPoint>(0.3);
protected readonly rows = Array.from({ length: 30 }, (_, i) => i + 1);
}Responsive#
matchMedia.
Make changes to your profile here.
Make changes to your profile here.
import { Component, afterNextRender, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsDialogComponent } from '@wds/angular/dialog';
import { WdsDrawerComponent } from '@wds/angular/drawer';
@Component({
selector: 'app-drawer-responsive-demo',
imports: [WdsButtonComponent, WdsDialogComponent, WdsDrawerComponent],
template: `
<wds-button variant="outline" (click)="isDesktop() ? dialog.show() : drawer.show()">Edit profile</wds-button>
<wds-dialog #dialog heading="Edit profile" description="On a wide screen: a dialog.">
<p class="m-0 text-sm text-muted-foreground">Make changes to your profile here.</p>
<wds-button slot="footer" (click)="dialog.close()">Save changes</wds-button>
</wds-dialog>
<wds-drawer #drawer handle heading="Edit profile" description="On a narrow screen: a drawer from the bottom.">
<p class="m-0 text-sm text-muted-foreground">Make changes to your profile here.</p>
<wds-button slot="footer" (click)="drawer.close()">Save changes</wds-button>
<wds-button slot="footer" variant="outline" (click)="drawer.close()">Cancel</wds-button>
</wds-drawer>
`,
})
export class DrawerResponsiveDemo {
protected readonly isDesktop = signal(true);
constructor() {
// matchMedia exists only in the browser (not during prerendering).
afterNextRender(() => {
const query = window.matchMedia('(min-width: 768px)');
this.isDesktop.set(query.matches);
query.addEventListener('change', (e) => this.isDesktop.set(e.matches));
});
}
}Service#
WdsDrawerService.open() creates the drawer from a component: data goes to the component through WDS_OVERLAY_DATA, 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 drawer declared in the markup.
import { Component, inject, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_OVERLAY_DATA, WdsDrawerService } from '@wds/angular/drawer';
/** Content of the drawer: any component. */
@Component({
selector: 'app-goal-panel',
imports: [WdsButtonComponent],
template: `
<div class="flex items-center justify-center gap-4">
<wds-button variant="outline" size="icon" shape="circle" label="Decrease" (click)="goal.set(goal() - 10)">−</wds-button>
<div class="min-w-[5ch] text-center text-5xl font-bold" aria-live="polite">{{ goal() }}</div>
<wds-button variant="outline" size="icon" shape="circle" label="Increase" (click)="goal.set(goal() + 10)">+</wds-button>
</div>
`,
})
export class GoalPanel {
// config.data of open()
readonly goal = signal(inject<{ goal: number }>(WDS_OVERLAY_DATA).goal);
}
@Component({
selector: 'app-drawer-service-demo',
imports: [WdsButtonComponent],
template: `
<div class="flex flex-wrap items-center justify-center gap-3">
<wds-button variant="outline" (click)="setGoal()">Set goal</wds-button>
<span class="text-sm text-muted-foreground">Goal: {{ goal() }} calories/day</span>
</div>
`,
})
export class DrawerServiceDemo {
private readonly drawers = inject(WdsDrawerService);
protected readonly goal = signal(350);
protected setGoal(): void {
this.drawers
.open<GoalPanel, number>({
title: 'Move goal',
description: 'Set your daily activity goal.',
content: GoalPanel,
data: { goal: this.goal() },
handle: true,
okText: 'Submit',
cancelText: 'Cancel',
// The value returned here is emitted by closed.
onOk: (panel) => panel?.goal(),
})
.closed.subscribe((value) => {
// undefined: closed with Cancel, Escape, the backdrop or a swipe.
if (value !== undefined) this.goal.set(value);
});
}
}API References#
wds-drawer#
snapPoints).
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| placement | WdsDrawerPlacement | 'bottom' | |
| handle | boolean | false | |
| modal | boolean | true | false: no backdrop and no page lock (the page stays usable). Set it as a property. |
| 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' |
Properties
| Property | Description |
|---|---|
| snapPointsWdsDrawerSnapPoint[] | top/bottom only), e.g. [0.4, 1] or ['200px', 0.8]. |
| snapPointWdsDrawerSnapPoint | undefined | snapPoints). |
Events
| Event | Description |
|---|---|
| wds-snap-changeCustomEvent<WdsDrawerSnapChangeDetail> | |
| 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 | |
| handle | |
| header | |
| title | |
| description | |
| close | |
| body | |
| footer |
CSS custom properties
| Property | Description |
|---|---|
| --wds-drawer-size | |
| --wds-drawer-backdrop |
Accessibility#
Screen reader#
<dialog> named by heading and described by description (aria-labelledby, aria-describedby). Always give it a heading; hide it visually if the design has no title.
close-label (default Zamknij, in Polish). The drag handle is decorative and hidden from screen readers.
hide-close, keep another way to close the drawer, such as a button in the footer; Escape also works in a modal drawer.
Keyboard support#
| Key | Function |
|---|---|
dismissible is false). A nested drawer closes first. |