The HTML dialog element has been around for quite a while, but only recently got a decent browser support , thanks to IE11 death and Safari updates. For those who need wider support range, Chrome team has a tiny polyfill available .
Let’s look at the examples of an accessible Dialog
component first to see what parts we need to “get inspired by”.
You noticed I pulled in “Controlled” example with open
state provided by the consumer. I believe that in most cases the consumers of the dialog will be controlling when and how the dialog is closed.
Similarly, React Spectrum by Adobe example:
Here, the API is a little different. React Spectrum always pass
close
callback for the consumer of the component to control the internalopen
state of theDialog
.
Enter native dialog
#
Approaching the API naïvely, we could do something like this:
By providing open
attribute to the <dialog>
, however, you are opting out from all the accessibility benefits <dialog>
can provide. No surprise, as MDN’s description of the property is:
The
open
property of theHTMLDialogElement
interface is a boolean value reflecting the open HTML attribute, indicating whether the<dialog>
is available for interaction. The property is now read only — it is possible to set the value to programmatically show or hide the dialog.
React sets open
attribute as the value changes, making the dialog
available for interaction. But that’s not really want we want. Let’s take another approach instead:
This is more verbose, but with this simple change, your <dialog>
already handles all the accessibility requirements of the dialog window:
- captures focus within the dialog, making other page content “invisible”;
- applies backdrop, clicking on which will close the dialog;
- Esc press will close the dialog;
- focus returns to the trigger element after the dialog is closed;
Give better controls #
In the simple implementation above, you could see that the Dialog
now requires passing hide
method. This can also be used to provide a consistent close button across all of your application:
The way you can leverage the fact that your consumers need to control the state of the dialog, is by providing a re-usable function of the controls state, to pass down to your Dialog
component:
Now the consumers of the Dialog
can simply use useDialogControls
and pass the controls
down to the Dialog
:
This approach is used by the upcoming Ariakit - Dialog and I love it: it’s DRY, concise and extensible (Ariakit version is also more optimised for passing controls around without causing rerenders).
useEffect
is ugly there
#
…can’t we just call
?
Well, you could, by reversing the control:
And then use it as before:
This might seem a bit more of an “inside out” approach, exposing dialogRef
onto the consumer of the dialog, but in the end, does it really matter?
Is that it? #
Technically yes, but also not. There’s a reason why RadixUI
and React Spectrum wrap the Dialog
into ContextProvider
and provide trigger Button
primitive. This allows connecting a trigger button to its dialog for screen readers at any depth.
All of them also provide a way to specify the description of the dialog for screen readers, like so:
As both elements can read from parent’s context, <dialog>
can be connected to the Heading
text via aria-labelledby
and id
to improve the accessibility:
Which you can also expect consumer to care about instead:
I always suggest to go to the libraries I mentioned and run their examples with screen readers, like Voice Over on Mac, to see how richer the experience becomes with proper accessibility hints. Luckily, all the libraries I mentioned are open-sourced and can be served as a great inspiration for your own component API.