Dialog/Modal

Modals present focused content that requires the user's attention without navigating away from the page.

Considerations

We use the native <dialog> element with showModal() to take advantage of built-in accessibility features like focus trapping, inert background behavior, and automatic announcement by assistive technologies.

Data attributes (data-dialogopen, data-dialogelement, data-dialogclose) are used to keep the JavaScript lightweight and decoupled from styling, making the component easy to reuse. The close button is always included and labeled for screen readers, and clicking outside the dialog closes it for an intuitive, user-friendly experience.

aria-modal="true" reinforces the modal's role for assistive technologies, ensuring that users understand they're in a temporary, focused overlay.

Remember to set autofocus to a close button or the first interaction. According to MDN:

The autofocus attribute should be added to the element the user is expected to interact with immediately upon opening a modal dialog. If no other element involves more immediate interaction, it is recommended to add autofocus to the close button inside the dialog, or the dialog itself if the user is expected to click/activate it to dismiss.

Examples

Basic Modal

<button type="button" class="button button--primary" data-dialogopen>
Open a modal
</button>
<dialog aria-modal="true" data-dialogelement>
<button class="button button--plain" type="button" autofocus data-dialogclose>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 18 18"
width="18"
height="18"
fill="currentColor"
role="img"
aria-labelledby="close-title"
>

<title id="close-title">Close</title>
<path
d="m10.815 9 6.793-6.793A1.284 1.284 0 1 0 15.793.392L9 7.185 2.207.392A1.283 1.283 0 0 0 .392 2.207L7.185 9 .392 15.793a1.283 1.283 0 1 0 1.815 1.815L9 10.815l6.793 6.793a1.283 1.283 0 0 0 1.815-1.815L10.815 9Z"
>
</path>
</svg>
</button>
<p>Hello there</p>
</dialog>
<script>
const openButtons = document.querySelectorAll("[data-dialogopen]");

openButtons.forEach((openButton) => {
const dialog = openButton.nextElementSibling;

if (!dialog || !dialog.matches("[data-dialogelement]")) return;

const closeButton = dialog.querySelector("[data-dialogclose]");

openButton.addEventListener("click", () => {
dialog.showModal();
});

if (closeButton) {
closeButton.addEventListener("click", () => {
dialog.close();
});
}

// Close when clicking outside dialog content
dialog.addEventListener("click", (event) => {
const rect = dialog.getBoundingClientRect();
if (
event.clientX < rect.left ||
event.clientX > rect.right ||
event.clientY < rect.top ||
event.clientY > rect.bottom
) {
dialog.close();
}
});
});
</script>

Slide-in Drawer Modal

<button type="button" class="button button--primary" data-dialogopen>
Open a slide-in modal
</button>
<dialog class="slide-in" aria-modal="true" data-dialogelement>
<button class="button button--plain" type="button" autofocus data-dialogclose>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 18 18"
width="18"
height="18"
fill="currentColor"
role="img"
aria-labelledby="close-title"
>

<title id="close-title">Close</title>
<path
d="m10.815 9 6.793-6.793A1.284 1.284 0 1 0 15.793.392L9 7.185 2.207.392A1.283 1.283 0 0 0 .392 2.207L7.185 9 .392 15.793a1.283 1.283 0 1 0 1.815 1.815L9 10.815l6.793 6.793a1.283 1.283 0 0 0 1.815-1.815L10.815 9Z"
>
</path>
</svg>
</button>
<p>Hello there</p>
</dialog>
<script>
const openButtons = document.querySelectorAll("[data-dialogopen]");

openButtons.forEach((openButton) => {
const dialog = openButton.nextElementSibling;

if (!dialog || !dialog.matches("[data-dialogelement]")) return;

const closeButton = dialog.querySelector("[data-dialogclose]");

openButton.addEventListener("click", () => {
dialog.showModal();
});

if (closeButton) {
closeButton.addEventListener("click", () => {
dialog.close();
});
}

// Close when clicking outside dialog content
dialog.addEventListener("click", (event) => {
const rect = dialog.getBoundingClientRect();
if (
event.clientX < rect.left ||
event.clientX > rect.right ||
event.clientY < rect.top ||
event.clientY > rect.bottom
) {
dialog.close();
}
});
});
</script>