(Modal) dialog windows
Dialogs vs Modals
A dialog box is a piece of UI that asks or requests the user for a response. This process of asking for the users' attention is non-obstrusive, they could ignore the request altogether. As Brian Dys puts it:
The option to cancel the action is the same as ignoring the Dialog. And that makes it a Non-modal Dialog.
A modal dialog, on the other hand, requires a users' response and makes it mandatory to interact with it. Focus, both literally and proverbially, can't be moved away from the modal. That makes "modal" a type of dialog. Something is a modal (meaning: a modal dialog), or something is modal (meaning: blocking). Content "behind" a modal dialog is inert, which means de-activated for any interaction. Users that are presented with an open modal dialog must not be able to interact with inert content. Very often users who perceive your web app visually will see an half-transparent overlay over the inert content, but behind the dialog.
Best practice
Placement of the modal in the DOM
As always, WAI-ARIA's Authoring Practices are a great resource on how to implement this abstract explanation into tangible code.
- At first, it is advised to distribute the role of either
dialog
(in case of non-modal dialogs) oralertdialog
to the element that contains all elements of the dialog - After that, you have to make sure that the keyboard focus is trapped within the modal dialog
- You have to supply at least two options to close the modal. One being a visible close button, the other being the ESC key. Often times, a click on a modal overlay (the one that is more or less hiding, sometimes darkens inert content) closes it as well.
- Up until now, the (modal) dialog has no accessible name. We can solve that by using either
aria-labelledby
and referring to a present headline that will label the dialog, oraria-label
. Read more about both strategies at Accessibility Developer Guide. - Finally, once the modal closes, keyboard focus must return to the triggering button.
Right now, we still lack a strategy to inform users of, e.g. screen readers, that once a modal is open, interaction and access should be limited to its content and interactions alone, and that navigating outside of the modal is prohibited. ARIA 1.1 has a solution for that -
placing aria-modal="true"
on the dialog container. The theory is that this won't allow access to inert content. In reality, there is a Webkit bug (affecting Voice Over on iOS and Mac which is preventing that using aria-modal suffices and becomes best practice.) So, for now, we have to rely on another strategy.
That strategy was actually a recommendation in ARIA 1.0, and it goes like this:
- At first, once a modal is open, set
aria-hidden="true"
on the "background", meaning: The part of the site that you want to render inaccessible or inert - You have to make sure that your modal dialog element is not a descendant of said inert background, because once you apply
aria-hidden="true"
on an element, all of its content and children will be removed from the accessibility tree. The catch is: You can't markup exceptions from this, for example placing an element witharia-hidden="false"
within it and expect it to somehow override the parents removal from the tree - unfortunalety, that is not how it works.
Which leads us to the required general markup (at least, for now):
<!-- That's the desired state of the live DOM when the modal is active/open -->
<div aria-hidden="true" inert>
<main role="main">
<p>content</p>
</main>
</div>
<div aria-hidden="false" role="alertdialog" aria-labelledby="myModal-title">
<h1 id="myModal-title">Supermodal!</h1>
...
</div>
<!-- In short: Inert content and modals are siblings, not nested! -->
Characteristics of the modal window itself
An accessible modal dialog must have the following characteristics, according to the WAI-ARIA Authoring Practices:
- Adding
aria-role="dialog"
to the dialog's outmost wrapper - The dialog has to have an accessible name, for example via
aria-labelledby="some-headlines-id"
- Must close on overlay click and ESC keystroke
- Once open, must trap focus, after closing focus must be sent back to the activating button
Using that best practice in Vue
The best practice for accessible modal dialogs consists of two parts: An easy implementation of portals and an easy implementation of the modal window itself.
The tool of choice for the first part is vue-portal, and it works like this:
<portal to="destination"><p>This slot content will be rendered wherever the
<code><portal-target></code> with name 'destination' is located.</p>
</portal>
<portal-target name="destination">
<!--
This component can be located anwhere in your App.
The slot content of the above portal component will be rendered here.
-->
</portal-target>
This means that you can "ship" a yet-to-be-selected dialog component in any other component we want (look into the example app's ProductTable
component for example) - and still you place it "outside" (or better: neighboring) the content container. Like this:
<some-wrapper>
<product-listing>
...
<portal to="modals">
<fancy-dialog-thingy>Hello!</fancy-dialog-thingy>
</portal>
</product-listing>
</some-wrapper>
<portal-target name="modals">
<!-- <fancy-dialog-thingy /> will be rendered here! -->
</portal-target>
The second part of the best practice is to choose a component for the (modal) dialog itself. Luckily accessible scripts like these are around, and one of them is Kitty Giraudel's a11y-dialog. Beside a React variant of this script there is also a Vue wrapper called vue-a11y-dialog. You can use this script as it is since its build with the best practices (mentioned above) in mind - but be sure to to set the disable-native
property to true
, since it otherwise renders a native dialog element (and this is not suitable to use in production as Scott O'Hara explains here).
Examples
- Placement of the portal region named "dialog" in Accessibook's high order
App
Component - Usage of both portal-target and vue-a11y-dialog in a component
Summary
- Use portal-vue when dealing with dialogs that are modal
- Use vue-a11y-dialog,
set disable-native="true"