Is it possible to build a modal dialog without any JavaScript at all?
In this article, we’ll look at two small experiments that explore what the platform can already do, and what small enhancements can improve the experience for real users.
Option 1: “what is the minimum needed for a modal to work?”
This example shows the minimum required for a modal dialog to work using native HTML. This uses the new dialog element and some experimental attributes - just to see how lean we can go! Focus moves to the first focusable element inside the dialog, which is one expected method.
Option 2: “what is the minimum we can do to make this modal easier to understand for real users?”
This example adds a small amount of JavaScript to move focus to the dialog heading instead. In user testing, this provided better context for screen reader users before they chose to interact with or close the modal. Again, we are still focusing on lean and experimental, not for real world use.
Option 1: Minimal version
Here is the basic HTML markup:
<main id="page">
<h1>A minimal viable modal - option 1</h1>
<button commandfor="demo-dialog" command="show-modal">
Open modal
</button>
</main>
<dialog id="demo-dialog" aria-labelledby="dialog-title">
<h2 id="dialog-title">Dialog heading</h2>
<button commandfor="demo-dialog" command="close">
Close
</button>
</dialog>
1. Open button
<button commandfor="demo-dialog" command="show-modal">
Open modal
</button>
commandfor="demo-dialog" links the button to the element with id="demo-dialog". It tells the browser which element the button will control.
More information on commandfor
command="show-modal" tells the browser what to do to that element. For a <dialog>, show-modal maps to opening it as a modal (equivalent to dialog.showModal()), so the browser handles the modal behaviour for you.
2. The dialog container
<dialog id="demo-dialog" aria-labelledby="dialog-title">
...
</dialog>
The <dialog> is a native dialog element, so you do not need role="dialog". It has an id of demo-dialog, so this is the element affected by the button.
aria-labelledby gives the dialog an accessible name by referencing the visible text in the modal heading.
More information on the <dialog> element
3. Heading
<h2 id="dialog-title">Dialog heading</h2>
The heading inside the modal can be used to provide a name for the <dialog>.
4. Close button
<button commandfor="demo-dialog" command="close">
Close
</button>
commandfor="demo-dialog" points to the same dialog element as the open button.
command="close" tells the browser to close the dialog (equivalent to dialog.close()), again without needing custom scripting.
This keeps open and close symmetrical, readable, and declarative.
Focus will return the original button on the page.
5. Dialog minimal styling
dialog {
border: 1px solid #000;
border-radius: 0.3em;
box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.3);
padding: 2.5rem 2rem 2rem;
max-width: 32rem;
width: calc(100% - 2rem);
}
This is purely visual. It makes the dialog boundary obvious and gives it a simple “floating surface” look. You can replace all of this with your own styles without affecting the dialog behaviour
6. Backdrop styling
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
dialog::backdrop styles the browser-provided backdrop for modal dialogs. This darkens the page and helps users understand that the dialog is the active layer.
Option 2: Slightly enhanced version
Here is the slightly enhanced HTML markup:
<main id="page">
<h1>A minimal viable modal - option 2</h1>
<button commandfor="demo-dialog" command="show-modal">
Open modal
</button>
</main>
<dialog id="demo-dialog" aria-labelledby="dialog-title">
<h2 id="dialog-title" tabindex="-1">Dialog heading</h2>
<button commandfor="demo-dialog" command="close">
Close
</button>
</dialog>
<scrip>
const dialog = document.getElementById('demo-dialog');
const title = document.getElementById('dialog-title');
dialog.addEventListener('toggle', () => {
if (dialog.open) {
title.focus();
}
});
</scrip>
7. Adding focus to the modal heading
<h2 id="dialog-title" tabindex="-1">
Dialog heading
</h2>
For the enhanced version, we will send focus to this heading to provide context for the modal.
User testing has shown that this is a better solution than sending focus to a focusable element within the modal. Often, these focusable elements do not provide enough context for the modal or identify its purpose.
tabindex="-1" makes the heading programmatically focusable but keeps it out of the normal TAB order. This is ideal when you want focus to land here once, on open, but not become an extra TAB stop during interaction.
8. A tiny script
<script>
const dialog = document.getElementById('demo-dialog');
const title = document.getElementById('dialog-title');
dialog.addEventListener('toggle', () => {
if (dialog.open) {
title.focus();
}
});
</script>
This script looks for the heading via the dialog-title ID and sends focus to this element.
9. Some optional focus CSS
#dialog-title:focus,
#dialog-title:focus-visible {
outline: 2px dotted #000;
animation: fade 3s forwards;
}
@keyframes fade {
0% {
background-color: #FFFF00;
outline-color: #000;
}
100% {
background-color: transparent;
outline-color: transparent;
}
}
This CSS is optional and included as a teaching aid.
When focus is moved to the heading (especially if it has tabindex="-1"), there is no default visible focus style. The brief highlight makes the focus move obvious, then fades away so it does not look like a permanent UI state.
Conclusion
This article focuses on what is required for a modal to work, and what markup could be used to make it more efficient.
What do you think? Could it be simpler? Would you add additional enhancements?
Caveats
- The examples in this article use the
commandforandcommandattributes, which are still experimental and not yet supported in all browsers. They are included here to explore what the platform is beginning to make possible, rather than as a ready-to-ship solution. - The term “minimum” focuses on moving away from complex markup and JavaScript, to explore what is possible using native HTML elements as the primary mechanism.
- This article does not make any claims about accessibility or support in various assistive technologies. It is literally an experiment in HTML markup.