Search topics
Confirmation dialogs that don't read out their question
A delete confirmation has no name and an unlinked message, so a screen reader announces only the Delete project button that focus lands on.
<div class="confirm" role="dialog" aria-modal="true"> The problem
Screen reader users land on Delete project without hearing what it will delete.
A list of projects has a Delete button on each row. Press Delete beside Spring launch. A dialog appears over the page: "Delete “Spring launch”?", a line saying the project and its 24 files will be deleted and that this can't be undone, and two buttons, Cancel and Delete project. It looks right. But focus is already on Delete project, and a screen reader says "dialog, Delete project, button" and stops there. Press Enter, as you would to move on, and the project is gone. The row went with the Delete button that opened the dialog, so focus has nowhere to return to and is left on the page's body, on nothing at all.
The dialog has a role but no name: its title is on screen, but nothing links it to the dialog, so a screen reader can announce it only as "dialog". ARIA requires every dialog to have a name, and a nameless one is reported under 4.1.2. The message that says what will happen is not linked either, so it is not read when focus moves in.
After Delete project, the script tries to return focus to the Delete button, but the row has just removed it. Focus falls to the body: a screen reader has nothing to announce, and where the next Tab goes depends on the browser. Losing focus like this fails 2.4.3.
Where focus starts is the rest of the problem. WCAG doesn't say which button a dialog should open on, but the ARIA Authoring Practices say that when the action can't be undone, focus should start on the least destructive one. A confirmation exists to stop a mistake. Starting on Delete project means the Enter press it is meant to catch is the one that goes through.
Anyone who works the page from the keyboard meets the dialog with focus already on Delete project. Mouse users have to move to a button and choose, so they rarely notice.
2 of 10 groups affected
- No vision (affected)
- Low vision (not affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (affected)
- Reach, strength (not affected)
- Cognitive (not affected)
- Seizures (not affected)
- Without vision A screen reader user hears “dialog, Delete project, button” and nothing about which project or how many files will go, then is left with focus on nothing after confirming.
- Limited manipulation A keyboard user who presses Enter twice, or whose key repeats, deletes the project before reading the dialog.
A scan sees role="dialog" and aria-modal="true" and may report nothing. axe's dialog-name check is a best practice, and it only sees dialogs that are open when the page loads. Neither a scan nor an AI reading the markup knows which button is the destructive one, or what happens to focus after a click that deletes the row it came from. Nothing looks wrong on screen, so a visual review passes it too. An AI asked to "add a confirmation" often writes exactly this one: it looks careful and puts focus on the confirm button.
Try it
Tab to a Delete button and press Enter, then press Enter again.
Watch which button has focus when the dialog opens, and read what a screen reader would say. After deleting, see where focus is.
Broken Can you fix this?
Projects
- Spring launch · 24 files
- Annual report · 9 files
- Team photos · 132 files
Screen reader says:
(Tab to a Delete button and press Enter)
The fix
Name the dialog after its title, link the message, start on Cancel, and move focus somewhere real afterwards.
- 1Name the dialog with
aria-labelledby, pointing at its title. - 2Link the message with
aria-describedby, and start focus on Cancel. - 3Once the item is gone, move focus to something that is still on the page.
<div id="page"> <h2>Projects</h2> <ul> <li data-name="Spring launch" data-files="24"> Spring launch · 24 files <button type="button" class="delete">Delete</button> </li> … </ul></div> <div class="confirm" role="dialog" aria-modal="true" hidden> <h2 class="confirm-title">Delete “Spring launch”?</h2> <p class="confirm-message">This deletes the project and its 24 files. It can't be undone.</p> <button type="button" class="cancel">Cancel</button> <button type="button" class="ok">Delete project</button></div>A native dialog
<div id="page"> <h2 id="projects-heading" tabindex="-1">Projects</h2> <ul> <li data-name="Spring launch" data-files="24"> Spring launch · 24 files <button type="button" class="delete">Delete</button> </li> … </ul></div> <dialog id="confirm-delete" role="alertdialog" aria-labelledby="confirm-title" aria-describedby="confirm-message"> <form method="dialog"> <h2 id="confirm-title">Delete “Spring launch”?</h2> <p id="confirm-message">This deletes the project and its 24 files. It can't be undone.</p> <button value="cancel" autofocus>Cancel</button> <button value="delete">Delete project</button> </form></dialog>Why this fix: Use native HTML first
On screen it matches the fixed versions word for word, and the page behind it is inert and Escape works, so it passes a look and a quick keyboard check. But nothing links the title or the message to the dialog, focus starts on Delete project, and after it, trigger.focus() aims at a button that has just been removed.
role="alertdialog" tells a screen reader this dialog needs an answer, aria-labelledby gives it the title as its name, and aria-describedby has the message read with it. autofocus puts focus on Cancel, so a stray Enter keeps the project. showModal() makes the page inert and closes on Escape. Once the row is gone, focus moves to the list's heading instead of falling to the body.
Both pass. Use the native dialog: the browser makes the page inert, closes on Escape and returns focus after Cancel, so the only code left is the part this guide is about. The custom dialog is the smallest change when a framework or design system already draws its own overlay, as in the "Before". If you use a library's confirm dialog, check that it lets you name both buttons and choose which one gets focus first.
The cost of the action decides what the dialog should look like.
- Can be undone: skip the dialog. Delete at once and offer Undo where the item was, as in toasts that vanish before you can read or undo them.
- Can't be undone: confirm, start on Cancel, and say what will be lost, with counts where they help ("its 24 files").
- Can't be undone and affects other people, such as deleting a shared workspace: ask people to type the item's name before the delete button works, and say in the dialog why that button is not available yet.
Write the title and the confirm button as the action and the item, as here: "Delete “Spring launch”?" and "Delete project". The name a screen reader reads is only as good as the title it points at, and "Are you sure?" with "OK" leaves everyone guessing.
window.confirm() is modal and read by screen readers, but browsers usually start it on OK and its buttons can only be OK and Cancel, so it can't match these dialogs.
Put Cancel first in the markup if you show it first, so Tab follows the order on screen. If the confirm button is styled red, make sure it doesn't rely on red alone: its label already says "Delete project".
After deleting, the list heading is a safe place for focus. The next row's Delete button is quicker for someone clearing several items, but it is easy to press by mistake, so don't start there. When nothing on the page says the item is gone, add a message that is announced, as in status messages that are shown but never announced.
For the rest of a modal's behaviour (focus moving in, the page behind it, Escape), see modals that leave keyboard focus behind them.
Fixing this with an AI coding assistant? Get this guide as Markdown
Verify the fix
5 checks, no mouse.
Test with: Keyboard, Screen reader, Zoom, Forced colors
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the shared dialog component changes.
Limits
This is a starting pattern, not a guarantee for every application. The snippets delete only from the page and leave out the request to the server, its errors, and what happens if it fails. Screen readers differ in how much of an alert dialog they read on their own, so test the complete journey with your target assistive technology.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
Keep this fixed as your code changes.
Turn guides like this into rules your coding agents and CI follow, then have the result retested with a keyboard and screen readers.