Modals - an accessibility approach
Modals are today a very common element but not yet fully supported: a lot of effort is required to implement them: here the steps I follow.
Modals should be used sparingly, potentially only when we want our user to read something or perform a blocking step in the current task without interrupting the graphic continuity of the page, but interrupting user action/activity until the task is completed or when there are steps the user needs to do before the task can be completed. Using a modal window instead of a full page allows users to maintain the context of their task.
Anatomy of a modal
Let's see which are the characteristics of a properly designed modal in its basic form and logic.
overlay: between our modal and the main page there is a layer that helps to graphically separate the two. In my example I'll use both a semi-transparent color and a blur effect. If you're using a
<dialog>
element you'll have this "for free" with the::backdrop
pseudo element (see below). Instead, if you're using a common<div>
you could consider using a wrapper/container for the modal to use it in its place.dialog: using the most appropriate ARIA tags, we must clarify whether it is an
alert
dialog or amodal
dialog (read below to pick the right one). This will help us also in setting the properaria-live
attribute (it's already implicit in our alert/modal role!). If possible, aim at using a<dialog>
element: dialogs expose some interesting and very specific methods via JS that could be worthy of considering.modal labeling: with the help of an
aria-labelledby
set to the title and (optionally) anaria-describedby
set to the content, we'll announce immediately to the user what's up with that box (you could also provide a visually hidden text with modal interaction instructions linked toaria-describedby
). If you don't want / can't add a specificid
to the title, or if you don't have any title at all, you can opt for anaria-label
attribute. Do you know how to Label like a pro, right? If you don't check this article!size: modals should fill the entire screen in small screens, to block the underlying page scroll and improve readability. For larger screens, we can improve its visibility by overlaying the content with an opaque (or partially opaque) element (above, the overlay/container bullet point), that helps the modal to pop out more and gain further relevance (see "overlay" above).
I suggest setting
height
andwidth
to around 90%, to let see a bit of the underlying content below, further emphasising that the modal is above all the rest, and to set differentmax-size
values depending on your design for larger screens.header: required to clarify whether it is an alert dialog or a modal dialog, but also what a user can do.
closing button: a button is needed to close (usually a Close/X in the top right corner, with a size of at least 44x44px). Usually also clicking outside the modal dismisses it but, if the modal requires the user to add data, it's preferable to avoid this behaviour, because it leads to the risk of losing the work of data insertion by mistake. In this case, it will be better to add a “Cancel” button, instead, following the design principle of introducing friction whenever there is a risk of error.
main content: well... guess what it will contain?
footer: it's not needed, but mind that could be helpful in certain cases, mainly for design purposes
Alert, Dialog and Alertdialog roles
There are multiple possibilities for declaring the type of modal, using the role
attribute. Even if they may seem almost the same, since we're stepping up our code, it's better to clarify the differences: screen readers, assistive technologies and future tools still to come will leverage these roles more and more.
To not dilute this article too much, I strongly suggest you take a look at this article, which quickly goes through the differences.
Do not set dialogs to display: none
by default.
Using a <dialog>
element, your browser will handle the visibility issue by itself, but what if you're using a <div>
? To toggle dialog
visibility, it's better not to rely on display:none
: using iOS Safari + VoiceOver focus will not move to the element (at least at this moment). You could instead use visibility: hidden
and visibility: visible
. The difference between the two methods is that elements with visibility: hidden
continues to occupy space in the page content, while those with display: none
don't. This should not be a problem in our case, since we're positioning everything related to the dialog with position: absolute
or fixed
.
This CSS property is also animatable, so that's a plus if you need it.
If using the hidden
attribute to hide dialogs in their default state (which will ensures that if CSS is ever blocked, the dialogs won’t become visible – useful for a browser’s reader mode), you could modify thehidden
attribute style to mitigate this buggy behaviour:
[role="dialog"][hidden] {
display: block;
visibility: hidden;
}
display: block
undoes the hidden
attribute’s default display: none
CSS, and visibility: hidden
manages the modal for it’s inactive state.
Grabbing focus
When moving focus into a container with role=”dialog”
or “alertdialog”
, the screen reader announces the role
and the accessible name (aria-label
/aria-labelledby
) and description (aria-describedby
).
The ARIA 1.1 Spec alertdialog (role) specifies that “when the alert dialog is displayed, authors should set focus to an active element within the alert dialog, such as a form edit field or an OK button.”
When moving focus inside, be sure to have some kind of reference to the triggering element for the opening, because as per specifications, after the modal is closed, focus should return to it or, at least, to the most appropriate element following the flow of logic.
About the triggering element, just a quick note: Aria provides the aria-haspopup
property, potentially useful but not yet broadly supported: in this case is better not to use it and wait for a better future!
NVDA Forms Mode Behavior & Insert + Spacebar Key
This is a really important bit to know: in a normal role=”dialog”
or “alertdialog”
NVDA automatically switches into Form Mode when the focus is sent into the dialog
, meaning that users can only use TAB to move inside the dialog and not the up/down arrow keys to read through the dialog
with linear navigation: to exit form mode NVDA users have to press Insert + Spacebar combo.
Keep that in mind and if you think it could be an important feature in your application, think to give the user quick advice about this, potentially using a visually-hidden text field. If your client uses extensively/requires the test for NVDA this is an important deviation to be aware of.
Mechanics set-up
Modal means that the user can’t interact with any content outside of the dialog
. So the keyboard, mouse, and screen reader interactions MUST be trapped inside the dialog
itself. We've already seen how to trap focus inside a container in a previous article, and here we'll see a complementary action to power up our mechanics: since we've coded our modal as a sibling of the main content, we'll proceed in removing focusability of the disabled main content underneath the dialog.
From the example layout I've coded, you can see I included a button within the main content which will activate the modal.
Upon clicking the modal-activating button, all the main content should become inaccessible: for assistive technologies, we'll set an
aria-hidden='true'
attribute so that a screen reader cursor will ignore it as a whole, for keyboard navigation, we will trap focus with JS, for mouse users we will setpointer-events: none
(to avoid possible click-triggered actions) andoverflow: hidden
(to prevent scrolling, not strictly needed but potentially a nice feature, so the main content will seem blocked as it is). Finally, the use ofaria-hidden="true"
andinert
together negate the ability for VoiceOver users to escape a modal dialog when reading line by line.The overlay becomes visible: visually it covers the main content, potentially with a partial see-trough effect (that you can achieve with a combination of semi-transparent
background-color
, blurred effect and shadows), and is able to receive a click for modal dismissal (if applicable, see "closing button" bullet point under "Anatomy" to refresh the concept). In the demo code, using a pseudo-selector for this scope, we will not be able to add an event listener to it. We'll listen to clicks not inside the modal element instead, making sure to remove it when closing.Additionally, the modal should get an
aria-hidden='false'
and go from havingdisplay: none
toblock
(or your preferred type).Move the focus on the first focusable element.
Upon closing the modal, make sure to return focus to the element that originally opened it or on the most logically appropriate element.
aria-modal
is meant to indicate to screen readers that only content contained within a dialog
with aria-modal="true"
should be accessible to the user. Safari + VoiceOver on both macOS and iOS have issues with it, making static content within a modal dialog inaccessible (see logged WebKit bug). Setting to false creates also some kind of bugs, but this is a behaviour easy to fix: instead of setting to false,
iyou can achieve the same result also by removing the attribute.
Some suggestions aimed at designers
Modals should be considered quick ways to gently interrupt the flow. With this in mind, carefully consider what to put inside a modal: it should not require access (or take a peek) at the underlying content and shouldn't contain too much content to require a scroll. A happy developer is one who has a clear and logical design to work on!
role="document"
was necessary at the time to work around a bug in, if I recall correctly, NVDA which assumed that a role="dialog"
only contained focusable elements and no content, and went directly into forms mode. This has likely been resolved since, but anyway, keep that in mind.