Accessible Modals
An accessible modal is a pop-up dialog that traps keyboard focus inside itself while open, closes on the Escape key, returns focus to the element that opened it, and announces itself to screen readers using the native <dialog> element or proper ARIA roles.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free HTML & CSS course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
By the end of this lesson you'll build a real, keyboard-friendly modal with the native element — focus trapped inside, Escape to close, focus returned to the trigger, and the page behind it locked.
Think of a modal as the passport-control booth at an airport. While you're at the booth, you can't wander off — the queue behind you is roped off (that's the backdrop ), you can only interact with the booth in front of you ( focus trapping ), and there's one clear way to leave (the officer waves you through, or you step back — that's Escape and the close button ). When you're done, you walk back to exactly where you were standing ( focus return ). The native element is a booth with all of that built in. Building a modal out of a plain is roping off the area yourself with no officer, no rope, and no exit sign — you have to construct every safety feature by hand, and beginners always miss one.
A modal is a window that appears on top of the page and demands attention before you can do anything else. For years, developers built them from a and then had to hand-write four hard things: trapping focus (the highlighted element keyboard users act on), handling the Escape key, adding ARIA roles so screen readers announce it, and dimming the rest of the page. Miss any one and you've shipped an inaccessible modal.
The native element does all four for you. When you open it with showModal() the browser traps focus (Tab cycles inside the dialog and never reaches the page), closes it on Escape , exposes role="dialog" and aria-modal="true" automatically, and paints a ::backdrop overlay while making the rest of the page inert (unclickable, untabbable). Your only jobs are to give it an accessible name with aria-labelledby , return focus to the trigger on close, and optionally lock scrolling.
One detail that trips everyone: show() and showModal() are not the same. show() opens a non-modal popup with no backdrop, no focus trap, and no Escape. showModal() is the one that gives you a real, accessible modal — use it every time you mean "modal".
1. The smallest correct modal
Read every comment, then run it. Click Open Modal , press Tab a few times (focus stays inside), and press Escape (it closes). All of that came from + showModal() — you wrote almost no behaviour code.
2. Returning focus and locking scroll
This adds the two things leaves to you: returning focus to the trigger when the modal closes, and locking background scroll while it's open. Notice the single close event handler does the cleanup — so it runs whether the user clicked a button, clicked the backdrop, or pressed Escape.
3. 🎯 Your turn: make it a real modal
Give the dialog an accessible name, open it as a true modal, and close it from the button. Each ___ has a 👉 hint right beside it, and the expected behaviour is in a comment at the bottom.
4. 🎯 Your turn: focus return + form auto-close
Make the form close the dialog on submit with method="dialog" , then return focus to the trigger in the close handler. Check your work against the ✅ Expected comment.
5. Mini-challenge: a delete confirmation
No blanks this time — just a comment outline. Build a "Delete item?" modal from scratch: native , showModal() , two buttons that close() , focus returned on close, and your own ::backdrop styling. The expected behaviour is listed in the comments.
Practice quiz
Which method opens a native dialog as a true modal?
- dialog.show()
- dialog.open()
- dialog.showModal()
- dialog.toggle()
Answer: dialog.showModal(). showModal() gives a backdrop, focus trapping, Escape-to-close, and the top layer. show() opens a non-modal popup with none of those.
What is the difference between show() and showModal()?
- They are identical
- show() traps focus; showModal() does not
- showModal() adds a backdrop, focus trap and Escape; show() is a non-modal popup with none of those
- show() renders in the top layer; showModal() does not
Answer: showModal() adds a backdrop, focus trap and Escape; show() is a non-modal popup with none of those. showModal() opens a real modal (backdrop, focus trap, Escape, top layer). show() is only a non-modal popup.
Which features does a native dialog opened with showModal() provide for free?
- Focus trapping, Escape-to-close, role="dialog"/aria-modal, and a ::backdrop
- Background scroll locking
- Automatic form validation
- A close button
Answer: Focus trapping, Escape-to-close, role="dialog"/aria-modal, and a ::backdrop. showModal() installs the focus trap, Escape handling, the dialog ARIA role/aria-modal, and paints the ::backdrop overlay automatically.
Do you need to add role="dialog" and aria-modal="true" yourself on a native dialog?
- Yes, always
- No — a dialog opened with showModal() exposes them automatically
- Only role="dialog" is automatic
- Only in older browsers
Answer: No — a dialog opened with showModal() exposes them automatically. A native dialog opened with showModal() already exposes role="dialog" and aria-modal="true". You only add an accessible name.
How do you give a dialog an accessible name from its heading?
- aria-describedby pointing at the heading
- aria-labelledby pointing at the heading's id
- title attribute on the dialog
- role="heading" on the dialog
Answer: aria-labelledby pointing at the heading's id. Add aria-labelledby pointing at the heading's id (or aria-label if there is no visible heading) so the dialog has a name.
Why should focus return to the trigger button when a modal closes?
- To reset the page scroll position
- So keyboard and screen-reader users keep their place in the focus order
- To re-run the open animation
- To clear the form fields
Answer: So keyboard and screen-reader users keep their place in the focus order. Returning focus to the trigger keeps keyboard/screen-reader users exactly where they were instead of dropping them at the top of the page.
What closes a native dialog automatically when a form inside it submits?
- form action="close"
- form method="dialog"
- form type="modal"
- form onsubmit="close"
Answer: form method="dialog". A form with method="dialog" closes the dialog on submit and fires its close event.
Which CSS pseudo-element styles the dim layer behind a modal dialog?
- dialog::before
- dialog::backdrop
- dialog::overlay
- dialog::after
Answer: dialog::backdrop. ::backdrop is the layer painted behind a dialog opened with showModal(); you can dim or blur it.
How do you stop the page behind the modal from scrolling?
- Add overflow: hidden to html while the modal is open and remove it on close
- Set the dialog to position: fixed
- Add a high z-index to the dialog
- Use aria-modal="true"
Answer: Add overflow: hidden to html while the modal is open and remove it on close. Scroll locking is the one piece dialog leaves to you: add overflow: hidden to html on open and remove it on close.
Why does Escape not close a custom div-based modal by default?
- Divs cannot receive keyboard events
- A plain div has no built-in Escape handling — only a native dialog opened with showModal() gives it for free
- Escape only works with role="dialog"
- The browser blocks Escape inside divs
Answer: A plain div has no built-in Escape handling — only a native dialog opened with showModal() gives it for free. A custom div modal needs a manual keydown listener. A native dialog opened with showModal() closes on Escape with no extra code.