Sitemap
Press enter or click to view image in full size

Accessibility by Default: Dialogs

--

Hi, and welcome to Accessibility by Default. If you’re new, this series is all about building web components with accessibility in mind from the very beginning — understanding what native HTML gives us for free, and where we need to add a little extra ourselves. If you’re a returning reader, welcome back!

This week we’re looking at dialogs, and to be honest this is probably the topic that I’ve debated more than any throughout my career. I’ve had many, many conversations with many, many people who say, “Dialogs aren’t accessible,” but I personally don’t think that’s true.

Poorly implemented dialogs aren’t accessible. Well-implemented dialogs can be.

I often see teams remove dialogs altogether in favour of navigating users to an entirely new page because it feels like the “accessible” option. In reality, that can introduce unnecessary friction by taking users away from the task they were in the middle of just to display a small amount of related information, before expecting them to navigate all the way back again.

I’ve also seen dialogs replaced with accordions. Sometimes that’s the right decision, but sometimes it simply means exposing far more content on the page than users actually need which can, in turn, make the page looked cluttered. A dialog can allow that additional information to remain hidden until it’s requested.

Like every component we’ve looked at in this series, dialogs aren’t inherently accessible or inaccessible. The difference comes down to how they’re built.

What is a dialog?

Sorry, I may have jumped the gun slightly there by going straight into dialog accessibility before actually explaining what a dialog is.

A dialog is a component that displays additional information or requests an action from the user without navigating them away from the current page. Dialogs are usually triggered by an action on the page, such as clicking a button or interacting with a control.

Once triggered, the dialog appears above the existing page content and temporarily becomes the focus of the user’s attention.

There are two common dialog patterns we need to understand:

  • Dialog — used for displaying additional information or collecting input from a user.
  • Alert dialog — used for important messages that require the user’s attention before continuing.

Dialogs can also have different behaviours:

  • Modal dialogs prevent interaction with the rest of the page until the dialog is closed.
  • Non-modal dialogs allow users to continue interacting with the rest of the application.

A dialog might be used for many different reasons. This could be editing information, displaying additional details, or allowing a user to complete a task without leaving the current page.

A common example is an enterprise application such as a CMS system. A database record might contain many fields:

  • ID
  • First name
  • Last name
  • Phone number
  • Address
  • Postcode
  • Country
  • Additional metadata

However, the purpose of a user interface isn’t always to expose every available piece of data.

Displaying every field in a table would quickly become overwhelming and make scanning difficult. Instead, the table might display the information users need most frequently, such as the ID, name and phone number, with an action allowing the user to open a dialog containing the additional information when required.

This allows users to quickly scan the information they need while still providing access to the full record when requested.

<button>
View information for Josh Allan
</button>

Accessibility isn’t about showing everything to everyone at all times. It’s about making sure users can access the information they need in a way that matches their task.

Modal dialogs

A modal dialog is a dialog that temporarily prevents interaction with the rest of the page until the user has completed an action or closed the dialog.

When a modal opens, the user’s focus is moved away from the page and into the dialog. While the dialog is open, users should not be able to interact with the content behind it.

Common examples include:

  • confirmation messages
  • login forms
  • editing details
  • checkout steps
  • destructive actions

For example:

“Are you sure you want to delete this customer?”

This isn’t something where we want the user accidentally continuing to interact with the page behind it. The modal creates a temporary interruption where the user must make a decision before continuing.

Alert dialogs

So if a modal dialog requires a user’s attention, what makes it different from an alert dialog?

In reality, they can overlap.

A modal describes how a dialog behaves. An alert dialog describes the importance and purpose of the information being presented. An alert dialog intentionally interrupts the user’s workflow because the information requires immediate attention.

Common examples include:

  • a critical error that prevents the user from continuing
  • a warning about an action’s consequences
  • a security-related message
  • a payment failure

For example:

“Your session has expired. Please sign in again to continue.”

or:

“Your payment could not be processed. Please update your payment method before placing your order.”

These are different from a standard dialog where the user has intentionally requested more information.

For example:

Dialog

  • Use case: Viewing customer details
  • Purpose: Provide additional information.
  • Use case: Editing profile details
  • Purpose: Allow user input.

Alert dialog

  • Use case: Payment failed
  • Purpose: Require immediate attention.
  • Use case: Session expired
  • Purpose: Inform the user they must act before continuing.

A common mistake is using alert dialogs for every message because they feel like the more accessible option.

However, alert dialogs are designed to interrupt the user. Overusing them can create a frustrating experience.

Native HTML dialogs

As with every component we’ve looked at throughout this series, the first question should always be:

“Does the platform already provide this behaviour?”

Modern browsers provide the native <dialog> element.

<dialog>
<h2>
Customer details
</h2>
<p>
Additional customer information.
</p>
</dialog>

The native element provides many behaviours developers often recreate manually, including modal behaviour through:

dialog.showModal();

and closing through:

dialog.close();

That doesn’t mean accessibility is automatically solved. You still need to consider accessible names, focus management and the content inside the dialog.

However, starting with native HTML gives you a much stronger foundation.

The good thing about using

All of these are things to think about when implementing a custom dialog solution, but remember that native HTML gives you much of this for free out of the box.

Main accessibility points

As I mentioned earlier, many conversations I’ve had about dialogs are around whether they are accessible. The important thing is understanding the behaviours that need to exist.

The key areas are:

  • Opening a dialog
  • Focus trapping
  • Closing a dialog
  • Returning focus
  • Background inertness
  • Announcing dialogs

I’m aware that that does sound like a lot of different things to keep track of but once you read through, they do sound pretty obvious.

Opening a dialog

When a dialog is triggered by an action on the page, focus should move inside the dialog.

Get Josh Allan’s stories in your inbox

Join Medium for free to get updates from this writer.

The exact location depends on the purpose of the dialog.

For example:

  • A form dialog may move focus to the first input.
  • An information dialog may move focus to the heading.
  • A destructive confirmation may move focus to the least destructive action.

There is no single correct answer for every dialog. The important part is that focus moves into the dialog and the user immediately understands what has opened.

Focus trapping

Once focus has moved into a modal dialog, users should not be able to accidentally navigate behind it.

This is known as focus trapping.

Keyboard users and assistive technology users should be able to move between the controls inside the dialog, but pressing Tab at the final focusable element should return them to the first focusable element.

I’ve covered a simplified focus trap implementation in my first accessibility article.

// Note: This is a simplified conceptual model to demonstrate the mechanics. 
// In production architecture, use a battle-tested primitive (like Radix or
// React Aria)
// to handle edge cases like shadow DOM, hidden elements, and Safari bugs.

/**
* Returns a list of elements within the specified element that can be focused
* @param {HTMLElement} containerElement Parent element to search inside
* @return {NodeList} The focusable elements within containerElement
*/
const getFocusableElements = (containerElement) => {
return containerElement.querySelectorAll(
`a[href], area[href], input:not([disabled]),
select:not([disabled]), textarea:not([disabled]),
button:not([disabled]), [tabindex="0"]`,
);
};

// Focus the first focusable element if it exists
if (focusableElements.length > 0) {
focusableElements[0].focus();
}

/**
* Handles tab key event with given focusable elements
* to keep focus within the modal.
*/
const handleTabKey = (event, activeElement, focusableElements) => {
if (event.key !== 'Tab') return;
if (focusableElements.length < 2) {
event.preventDefault();
return;
}
if (event.shiftKey && activeElement === focusableElements[0]) {
event.preventDefault();
focusableElements[focusableElements.length - 1].focus();
} else if (
!event.shiftKey &&
activeElement === focusableElements[focusableElements.length - 1]
) {
event.preventDefault();
focusableElements[0].focus();
}
};

The important concept is:

  • Focus enters the dialog.
  • Focus stays within the dialog.
  • Focus returns when the dialog closes.

If you’re using the native <dialog> element with showModal(), this is handled for you automatically.

Closing a dialog

Dialogs should provide clear ways for users to close them.

Common methods include:

  • A close button.
  • Pressing Escape.
  • An optional backdrop click.

Backdrop clicking should be used carefully. Some dialogs contain important information or forms where accidental dismissal could cause frustration.

When a dialog closes, focus should return to the element that opened it.

If a user clicked a button to open a dialog, that button should regain focus once the dialog closes.

This allows users to continue exactly where they left off.

I still remember the first custom modal I built. During testing, our tester repeatedly pressed Enter to open the modal, checked that focus moved to the first focusable element (the close button in this case), activated it, then verified that focus returned to the original trigger. It felt repetitive at the time, but it’s exactly the kind of behaviour that’s easy to miss when you’re only testing with a mouse.

Background inertness (inert vs aria-hidden)

One of the most common mistakes with modal dialogs is only thinking about what users can see.

A modal might visually appear above the page, but if the content behind it is still available to keyboard users, then the experience is broken.

For example:

<div class="modal">
<h2>
Delete customer?
</h2>

<button>
Cancel
</button>

<button>
Delete
</button>
</div>

Visually, this may look like a modal.

However, a keyboard user can still potentially press Tab and move through the page behind the dialog.

This creates confusion because the user has entered a modal state visually, but the browser still allows interaction with the content underneath.

This is where the inert attribute becomes useful.

<main inert>
Page content
</main>

The inert attribute tells the browser that the content should not:

  • receive focus
  • respond to user interaction
  • be exposed to assistive technology

A common older approach was using:

aria-hidden=”true”

However, these solve different problems.

aria-hidden only removes content from the accessibility tree. It does not prevent keyboard users from reaching interactive elements.

For example:

<button aria-hidden="true">
Submit
</button>

The button may still be focusable even though assistive technology cannot announce it.

For modal dialogs, the goal is not just hiding content from screen readers. The goal is preventing interaction with the rest of the page.

This is why inert is the better solution.

Native <dialog> opened via showModal() also makes the rest of the page inert automatically — you don't need to add this yourself.

Announcing dialogs

Moving focus into a dialog is only part of the experience. Users also need to understand what has opened and why.

A dialog needs an accessible name.

The preferred approach is usually using aria-labelledby connected to a visible heading.

Example:

<dialog aria-labelledby="customer-details-title">
<h2 id="customer-details-title">
Customer details
</h2>

<p>
Additional information about this customer.
</p>
</dialog>

A screen reader can now announce something like:

“Customer details, dialog”

rather than simply:

“Dialog”

which provides no useful context.

If there isn’t a visible heading, you can provide a name using:

aria-label

Example:

<dialog aria-label="Delete customer">

However, visible text is generally preferred because it keeps the visual and accessible experiences aligned.

You can also provide additional context using:

aria-describedby

Example:

<dialog aria-labelledby="delete-title" aria-describedby="delete-description">

With:

<h2 id="delete-title">
Delete customer
</h2>

<p id="delete-description">
This action cannot be undone.
</p>

A screen reader user receives:

  • the name of the dialog
  • the purpose of the dialog
  • any additional instructions

The goal is the same as with every accessible component:

The user should understand what they are interacting with, what state they are in, and what action they need to take.

Key takeaways

Accessible dialogs come down to a few core principles:

  • Use the correct dialog pattern for the situation.
  • Move focus into the dialog when it opens.
  • Keep focus within modal dialogs.
  • Return focus when the dialog closes.
  • Prevent background interaction.
  • Provide a clear accessible name.
  • Prefer native HTML where possible.

Like every component we’ve looked at in this series, accessibility is rarely about adding more complexity. Often, it is about understanding the behaviour users need and using the tools the platform already provides.

If there’s one thing I hope you take away from this article, it’s that dialogs aren’t inaccessible — they’re just one of the easiest components to implement incorrectly. Understanding focus management, user context and native browser behaviour will get you much further than trying to avoid dialogs altogether.

Accessibility by Default is a series where I explore practical accessibility techniques from the perspective of a frontend engineer. Rather than repeating specifications, the goal is to explain the engineering decisions and patterns that make accessible interfaces easier to build from the start.

You can find the full series on my profile

--

--

Lead Frontend Engineer | React & Next.js Specialist | Accessible Web Applications | Design Systems

See more from me