Skip to main content

Modal

<zeno-modal>OverlaysSource

A dialog in the middle of the page that 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​

Confirm an action​

Call show() to open the modal. close(value) closes it and the close event carries that value.

Delete report

Nothing chosen yet.

The file is removed for everyone. This cannot be undone.

Cancel Delete
<zeno-button variant="destructive">Delete report</zeno-button>
<p data-result>Nothing chosen yet.</p>

<zeno-modal heading="Delete report.pdf?" size="sm">
<p>The file is removed for everyone. This cannot be undone.</p>
<zeno-button slot="footer" variant="ghost" value="cancelled">Cancel</zeno-button>
<zeno-button slot="footer" variant="destructive" value="deleted">Delete</zeno-button>
</zeno-modal>
const modal = document.querySelector('zeno-modal');
document.querySelector('zeno-button').addEventListener('click', () => {
modal.returnValue = '';
modal.show();
});
document.querySelectorAll('zeno-button[slot="footer"]').forEach(button => {
button.addEventListener('click', () => modal.close(button.value));
});
modal.addEventListener('close', event => {
document.querySelector('[data-result]').textContent =
'Closed with: ' + (event.detail.returnValue || 'no value');
});

Sizes​

The size sets the width. On a screen narrower than 640px every modal fills the screen.

Small Medium Large Extra large

Press Escape or use the close button to close this modal.

<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="xl">Extra large</zeno-button>
</div>

<zeno-modal heading="Keyboard shortcuts">
<p>Press Escape or use the close button to close this modal.</p>
</zeno-modal>
const modal = document.querySelector('zeno-modal');
document.querySelectorAll('[data-openers] zeno-button').forEach(button => {
button.addEventListener('click', () => {
modal.size = button.value;
modal.show();
});
});

A choice that must be made​

With no-close-button, no-backdrop-close and no-escape-close only the buttons in the footer close the modal.

Review terms

The terms of service changed on 1 June. Accept them to keep using your account.

Sign out Accept
<zeno-button variant="outline">Review terms</zeno-button>

<zeno-modal heading="Updated terms" no-close-button no-backdrop-close no-escape-close>
<p>The terms of service changed on 1 June. Accept them to keep using your account.</p>
<zeno-button slot="footer" variant="ghost">Sign out</zeno-button>
<zeno-button slot="footer">Accept</zeno-button>
</zeno-modal>
const modal = document.querySelector('zeno-modal');
document.querySelector('zeno-button').addEventListener('click', () => modal.show());
document.querySelectorAll('zeno-button[slot="footer"]').forEach(button => {
button.addEventListener('click', () => modal.close());
});

Ask before closing​

The close button, the backdrop and Escape fire request-close first. Cancel the event to keep the modal open.

Write a note

No close request yet.

Save note
<zeno-button variant="outline">Write a note</zeno-button>
<p data-result>No close request yet.</p>

<zeno-modal heading="New note">
<zeno-textarea label="Note"></zeno-textarea>
<zeno-button slot="footer">Save note</zeno-button>
</zeno-modal>
const modal = document.querySelector('zeno-modal');
const result = document.querySelector('[data-result]');
document.querySelector('zeno-button').addEventListener('click', () => modal.show());
document.querySelector('zeno-button[slot="footer"]').addEventListener('click', () => modal.close());
modal.addEventListener('request-close', event => {
const note = document.querySelector('zeno-textarea').value;
if (note) {
event.preventDefault();
result.textContent = 'Kept open: the note is not saved. Asked by: ' + event.detail.source;
} else {
result.textContent = 'Closed by: ' + event.detail.source;
}
});

Attributes and properties​

PropertyAttributeTypeDefaultDescription
sizesizereflects'sm' | 'md' | 'lg' | 'xl' | '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 modal opened.
close{ returnValue: string }The modal 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 modal 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 modal. Focus does not leave the modal while it is open.
EscapeAsks the modal to close. Does nothing when no-escape-close is set or a request-close listener cancels the event.
EnterSpaceOn the close button, asks the modal 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 modal, 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 modal is open.
  • With reduced motion the modal 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 26 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 modal 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 modal 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 modal opens again. A later close by Escape, the backdrop or the close button reports the value of the earlier close(value) call.
  • size="full" does not fill a wide screen. The width and height are still limited by the margin around the modal, and the corners stay rounded.
  • The scroll lock hides the page scrollbar and does not keep its space, so the page can shift sideways while the modal is open.