Build accessible dialogs/modals in the frontend
72
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Create a modal that is understandable and operable with a keyboard and assistive technology. Keep the application’s existing framework and visual language unless the user asks for broader changes.
For the rationale and source links behind these requirements, read references/research.md.
<dialog> with showModal() for a true modal. It provides the modal behaviors required below — focus, inertness, containment, Escape, and focus restoration — without custom code.role="dialog" implementation only when native dialog or the existing primitive cannot meet the product or browser constraints. A custom dialog must reproduce modal behavior; adding ARIA attributes alone is insufficient.role="alertdialog" only for brief, urgent messages that interrupt the user and require an immediate response, such as an irreversible confirmation.<dialog id="confirm" aria-labelledby="confirm-title">
<form method="dialog">
<h2 id="confirm-title">Delete this item?</h2>
<p>This cannot be undone.</p>
<button type="submit" value="cancel">Cancel</button>
<button type="submit" value="delete">Delete</button>
</form>
</dialog>
<script>
const dlg = document.getElementById('confirm');
// Open as a true modal: focus moves in, background becomes inert, Escape closes.
dlg.showModal();
</script>A visible close control is provided by the Cancel button (a method="dialog" form submits and closes the dialog). Apply the semantics, focus, and verification sections below on top of this baseline.
aria-labelledby; otherwise use a meaningful aria-label. Do not provide both unless they serve distinct, intentional purposes.aria-describedby only for a short, simple description. Omit it when the content contains paragraphs, lists, tables, or other structure that would be hard to hear as one concatenated announcement.showModal(), do not add redundant ARIA or tabindex attributes to the <dialog> element. In particular, do not put tabindex on a native <dialog>.role="dialog" and aria-modal="true" only when the background is genuinely inert for every user: it must be visually obscured and impossible to reach by pointer or keyboard. Keep the dialog outside any ancestor hidden from assistive technology.tabindex="-1" for long/structured content, or the least destructive action for an irreversible decision.Tab and Shift+Tab within a custom modal. Native modal dialogs handle this containment; do not add a second competing focus trap without evidence it is needed.Escape to close a modal, unless the product explicitly requires a different, documented behavior. Ensure only the topmost dialog closes when dialogs can stack.tabindex values. Preserve a logical DOM and focus order.<label> and connect validation text with aria-describedby (and aria-errormessage only when the project’s support strategy justifies it). Set aria-invalid="true" while an error is present.Inspect the implementation and, when the project supports it, add or update automated tests for the primary workflow. Verify:
Tab/Shift+Tab cannot reach the page behind a custom modal; the background is inert for native or custom implementations.Escape and the visible close/cancel button work as intended.If a framework or dialog library changes one of these behaviors, document the exception and test the resulting behavior rather than assuming the library is correct.