Skip to main content

Drawer

<zeno-drawer>OverlaysSource

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.

Open filters

Show orders from the last 30 days.

Done
<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.

Start End Top Bottom

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.

Small Medium Large Full

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().

Edit profile

Nothing saved yet.

Cancel Save changes
<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​

PropertyAttributeTypeDefaultDescription
placementplacementreflects'start' | 'end' | 'top' | 'bottom''start'The edge the drawer is attached to.
sizesizereflects'sm' | 'md' | 'lg' | 'full''md'
openopenreflectsbooleanfalseWhether the dialog is showing. Set it, or call show() and close(). From ZenoDialogElement.
headingheadingstring''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.
noCloseButtonno-close-buttonbooleanfalse From ZenoDialogElement.
noBackdropCloseno-backdrop-closebooleanfalseKeeps the dialog open when the backdrop is pressed. From ZenoDialogElement.
noEscapeCloseno-escape-closebooleanfalseKeeps the dialog open when Escape is pressed. From ZenoDialogElement.
closeLabelclose-labelstring'Close'The name of the close button. From ZenoDialogElement.
returnValuestring''What close() was given, as on a native dialog. From ZenoDialogElement.
testIdstringTest ID for automated testing From ZenoElement.

Methods​

MethodDescription
show(): void From ZenoDialogElement.
close(returnValue?: string): void From ZenoDialogElement.
focus(): voidFocus the component or its first focusable element From ZenoElement.
blur(): voidBlur the component or its focused element From ZenoElement.

Events​

EventDetailDescription
openThe 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​

SlotDescription
DefaultThe content.
headerHeading content, in place of the heading text.
footerActions. The footer is hidden while this slot is empty.

CSS parts​

PartDescription
baseThe dialog element.
panelThe column that holds header, body and footer.
headerThe header.
headingThe heading.
closeThe close button.
bodyThe scrolling content area.
footerThe 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​

KeyWhat it does
TabShift+TabMoves focus between the controls inside the drawer. Focus does not leave the drawer while it is open.
EscapeAsks the drawer to close. Does nothing when no-escape-close is set or a request-close listener cancels the event.
EnterSpaceOn 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 with showModal(), 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-labelledby points at an <h2> that holds the heading text or the header slot. Without a heading, the aria-label of the element is copied to the dialog.
  • The close button is a native <button> named by close-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, no header slot content and no aria-label has no accessible name, and the element does not warn about it.
  • The aria-label is read when the drawer renders. Changing only aria-label later does not update the dialog until something else makes it render again.
  • The heading is always an <h2>. The level cannot be changed.
  • returnValue is not cleared when the drawer opens again. A later close by Escape, the backdrop or the close button reports the value of the earlier close(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.