Drawer
A panel that slides in from an edge of the page and needs an answer before the page behind it can be used again. Built on a native modal <dialog>.
import '@usezeno/tokens/css';
import '@usezeno/core/web-components';
Examples
Basic drawer
Call show() to open the drawer and close() to close it. The close button, the backdrop and Escape also close it.
Show orders from the last 30 days.
<zeno-button variant="outline">Open filters</zeno-button>
<zeno-drawer heading="Filters">
<p>Show orders from the last 30 days.</p>
<zeno-button slot="footer">Done</zeno-button>
</zeno-drawer>
const drawer = document.querySelector('zeno-drawer');
document.querySelector('zeno-button').addEventListener('click', () => drawer.show());
document.querySelector('zeno-button[slot="footer"]').addEventListener('click', () => drawer.close());
Placement
start and end follow the reading direction. top and bottom span the full width.
You have no new notifications.
<div data-openers>
<zeno-button variant="outline" value="start">Start</zeno-button>
<zeno-button variant="outline" value="end">End</zeno-button>
<zeno-button variant="outline" value="top">Top</zeno-button>
<zeno-button variant="outline" value="bottom">Bottom</zeno-button>
</div>
<zeno-drawer heading="Notifications">
<p>You have no new notifications.</p>
</zeno-drawer>
const drawer = document.querySelector('zeno-drawer');
document.querySelectorAll('[data-openers] zeno-button').forEach(button => {
button.addEventListener('click', () => {
drawer.placement = button.value;
drawer.show();
});
});
Sizes
On the start and end edges size sets the width. On the top and bottom edges it sets the height.
Order 1042 was sent on 3 March.
<div data-openers>
<zeno-button variant="outline" value="sm">Small</zeno-button>
<zeno-button variant="outline" value="md">Medium</zeno-button>
<zeno-button variant="outline" value="lg">Large</zeno-button>
<zeno-button variant="outline" value="full">Full</zeno-button>
</div>
<zeno-drawer heading="Order details" placement="end">
<p>Order 1042 was sent on 3 March.</p>
</zeno-drawer>
const drawer = document.querySelector('zeno-drawer');
document.querySelectorAll('[data-openers] zeno-button').forEach(button => {
button.addEventListener('click', () => {
drawer.size = button.value;
drawer.show();
});
});
A form that is not closed by accident
With no-backdrop-close a press outside the drawer does nothing. The close event carries the value given to close().
Nothing saved yet.
<zeno-button>Edit profile</zeno-button>
<p data-result>Nothing saved yet.</p>
<zeno-drawer heading="Edit profile" placement="end" no-backdrop-close>
<zeno-input label="Display name" value="Ada Lovelace"></zeno-input>
<zeno-button slot="footer" variant="ghost" value="cancelled">Cancel</zeno-button>
<zeno-button slot="footer" value="saved">Save changes</zeno-button>
</zeno-drawer>
const drawer = document.querySelector('zeno-drawer');
document.querySelector('zeno-button').addEventListener('click', () => {
drawer.returnValue = '';
drawer.show();
});
document.querySelectorAll('zeno-button[slot="footer"]').forEach(button => {
button.addEventListener('click', () => drawer.close(button.value));
});
drawer.addEventListener('close', event => {
document.querySelector('[data-result]').textContent =
'Closed with: ' + (event.detail.returnValue || 'no value');
});
Attributes and properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
placement | placementreflects | 'start' | 'end' | 'top' | 'bottom' | 'start' | The edge the drawer is attached to. |
size | sizereflects | 'sm' | 'md' | 'lg' | 'full' | 'md' | |
open | openreflects | boolean | false | Whether the dialog is showing. Set it, or call show() and close(). From ZenoDialogElement. |
heading | heading | string | '' | The heading, which also names the dialog. Use the header slot for
richer content, or aria-label when there is no visible heading. From ZenoDialogElement. |
noCloseButton | no-close-button | boolean | false | From ZenoDialogElement. |
noBackdropClose | no-backdrop-close | boolean | false | Keeps the dialog open when the backdrop is pressed. From ZenoDialogElement. |
noEscapeClose | no-escape-close | boolean | false | Keeps the dialog open when Escape is pressed. From ZenoDialogElement. |
closeLabel | close-label | string | 'Close' | The name of the close button. From ZenoDialogElement. |
returnValue | string | '' | What close() was given, as on a native dialog. From ZenoDialogElement. | |
testId | string | Test ID for automated testing From ZenoElement. |
Methods
| Method | Description |
|---|---|
show(): void | From ZenoDialogElement. |
close(returnValue?: string): void | From ZenoDialogElement. |
focus(): void | Focus the component or its first focusable element From ZenoElement. |
blur(): void | Blur the component or its focused element From ZenoElement. |
Events
| Event | Detail | Description |
|---|---|---|
open | The drawer opened. | |
close | { returnValue: string } | The drawer closed. detail.returnValue is what close() was given. |
request-close | { source: ('close-button' | 'backdrop' | 'escape') } | The close button, the backdrop or Escape asked to close; detail.source says which. Cancel it to keep the drawer open. |
Slots
| Slot | Description |
|---|---|
| Default | The content. |
header | Heading content, in place of the heading text. |
footer | Actions. The footer is hidden while this slot is empty. |
CSS parts
| Part | Description |
|---|---|
base | The dialog element. |
panel | The column that holds header, body and footer. |
header | The header. |
heading | The heading. |
close | The close button. |
body | The scrolling content area. |
footer | The footer. |
CSS custom properties
This element documents no custom properties of its own. Style it through its parts and through the tokens it reads.
The element's own source reads no tokens.
Accessibility
Keyboard
| Key | What it does |
|---|---|
| TabShift+Tab | Moves focus between the controls inside the drawer. Focus does not leave the drawer while it is open. |
| Escape | Asks the drawer to close. Does nothing when no-escape-close is set or a request-close listener cancels the event. |
| EnterSpace | On the close button, asks the drawer to close. |
Forms
This element is not a form control. It adds nothing to a form's data.
Notes
- Renders a native
<dialog>and opens it withshowModal(), so the browser moves focus into the drawer, keeps it there, makes the page behind inert and returns focus to the opener on close. - The dialog is named by its heading:
aria-labelledbypoints at an<h2>that holds theheadingtext or theheaderslot. Without a heading, thearia-labelof the element is copied to the dialog. - The close button is a native
<button>named byclose-label, which is “Close” by default. Its icon is hidden from assistive technology. - Page scrolling is locked while the drawer is open.
- With reduced motion the drawer appears and leaves without a transition. In forced colours it gets a border.
Tests and known gaps
- Tested in a real browser by
dialog.test.ts. - Checked with axe-core in the light and the dark theme.
- Has no Storybook story.
4 of the 27 properties, methods, events, slots and parts above have no description in the source
- the property
size - the property
noCloseButton - the method
show() - the method
close()
Known gaps:
- A drawer with no
heading, noheaderslot content and noaria-labelhas no accessible name, and the element does not warn about it. - The
aria-labelis read when the drawer renders. Changing onlyaria-labellater does not update the dialog until something else makes it render again. - The heading is always an
<h2>. The level cannot be changed. returnValueis not cleared when the drawer opens again. A later close by Escape, the backdrop or the close button reports the value of the earlierclose(value)call.- The scroll lock hides the page scrollbar and does not keep its space, so the page can shift sideways while the drawer is open.