Open sandrina-p opened 3 years ago
Edited on August 5th 2021 to include @headlessui/react to the comparison.
Hello Sandrina!
Thank you for the kind words, thatâs very sweet. Right, so I am familiar with the two libraries you mentioned (and some others). I would say the major difference is that they ultimately belong to an encompassing framework (@reach
, @react-aria
and @headlessui/react
respectively). So if you are using their toolbelts already, you might as well use their dialog implementation, since itâs included in.
Iâm going to assume you are in fact not using @reach
, @react-aria
or @headlessui/react
fully, so letâs break it down a little and see whatâs going on.
Letâs start by talking about file size. Here is the information gathered from bundlephobia:
Dialog
, and 3.7Kb into FocusTrap
.If you ask me, 9Kb for a dialog script is too much. The composition of the @react/dialog
package is interesting though: 20% of it is a package intended to lock the window scroll. This is something a11y-dialog
does not do natively, but offers a 2-lines solution in the documentation.
After that we have 30% in 2 focus trap packages, which is not too surprising since that is the core of a dialog. Still, 2.9Kb is a lot in my opinionâthatâs almost the entire size of react-a11y-dialog
. The only reason I can think of for why trapping the focus is that heavy is because it handles more cases than a11y-dialog (see focus trap below).
On @react-aria/dialog
âs side, 50% of its size comes from @react-aria/interactions
, a utility library providing all @react-aria
packages some handy hooks and functions to handle keyboard shortcuts and focus management. Do they need all of it in the dialog component/hook? Most likely not. I guess tree-shaking will shuffle down whatâs not needed, but still. That makes a lot of sense in the context of their overall framework though. The more packages you use from the framework, the more code can be reusedâthatâs a good approach for both of these libs.
Since there is no way to get only the dialog from @headlessui/react
, Iâd say only use it if youâre going to use more of its components otherwise that feels like an overpriced dialog honestly.
Trapping the focus within the dialog is probably whatâs hardest in building a dialog library. The basics are very easy to cover, but if you want to cover every single case, it takes a lot of effort. And thatâs because getting all focusable elements is way harder than it should be.
a11y-dialog uses focusable-selectors, a micro-package of my own, which exposes a CSS selector aiming at querying all the focusable elements. Unfortunately, it is not possible with CSS only, because of certains cases outlined in the documentation of another package attempting to do this more reliable, focus-trap/tabbable.
Looking at how focus-lock (the library used by @reach/dialog
) queries tabbable elements, I would say there is a lot they do not cover here (although the code is so fragmented, itâs a little hard to be certain). Naming just a few: audio
and video
should have the controls
attribute to be focusable, elements with the tabindex
attribute are only tabbable if the value is not negative, hidden inputs should not be considered tabbable and radio inputs are their own mess as well. That being said, the library itself is 2.5Kb, so I assume it does quite a lot of things, still.
Looking at FocusScope (the component used by @react-aria
), it looks a little more solid to me, but still itâs a lot of code, so I hope they do handle focusable elements more comprehensively than what a11y-dialog does, otherwise I wonder why one needs so much code.
The way @headlessui/react
finds focusable elements is similar to the one from a11y-dialog but omits quite a few cases, such as audio
and video
elements with the controls
attribute, and yields false positives like considering hidden inputs and visually hidden elements focusable. So not bad, but also not perfect Iâd say (if there is even such a thing), especially considering it ships almost 4Kb for that purpose only.
When it comes to making anything but the dialog itself âinertâ (as in, non discoverable/focusable), you have 2 big methods:
Setting aria-hidden="true"
to the content container (or containers, if your application is divided in multiple root containers) when the dialog is open, and turning it off when the dialog is closed. Similarly, the dialog (or its container) should have aria-hidden="true"
when closed so itâs also only usable when open.
Setting aria-modal="true"
on the dialog to essentially do the same thing and make it stand out.
As far as I know, the first method was there first, before the aria-modal
attribute was formally introduced, and has long been the most solid way to make it happen. My understanding is that as of now, both methods are equally fine.
aria-modal="true"
should be fine provided the focus is handled properly (which goes without saying).aria-modal
well but as of now, his company Temesis, recommends the aria-modal
approach to less mature development team because the implementation is easier than toggling aria-hidden
on a few containers.@react-aria/dialog
and a11y-dialog@6
use the aria-hidden="true"
strategy while @reach/dialog
, @headlessui/react
and a11y-dialog@7
all use the aria-modal="true"
strategy.
All that being said, if support with old assistive technologies is of any concern for you, you might want to stick to an implementation that switches the aria-hidden
attribute to be on the safe side.
It seems that all libraries but @headlessui/react
support using alert dialogs, which are dialogs that require user interaction and therefore cannot be closed via ESC or clicking on the backdrop. @reach
offers it as another package though (@reach/alert-dialog
), which weighs 9.7Kb (although it probably share a lot of code with the @reach/dialog
one so itâs going to be less than 19Kb in practice). I couldnât figure out how to do it with @react-aria/dialog
but their documentation says itâs possible, and their codebase mentions an AlertDialog
component, so maybe it just doesnât exist with the hook.
They also all provide some sort of event system to react to the dialog being open or closed, which is sometimes necessary to buid more complex interfaces.
I didnât really dig for nested dialogsâ support since itâs a pretty fishy design pattern anyway, but this is supported by a11y-dialog
under the hood. I donât know whether itâs handled properly by react-a11y-dialog
thoughâI never tried. @react-aria/dialog
mentions it in its documentation, so it must be supported. @headlessui/react
does not mention it in its documentation but supports it and @reach/dialog
doesnât mention it anywhere, so probably not.
All libraries provide decent flexibility overall: @reach/dialog
and @headlessui/react
do not have a hook, but they have several components for the container, the overlay, the dialog⊠react-a11y-dialog
exports both a component and a hook for more advanced use cases. @react-aria/dialog
provides the most flexibility with a lot of hooks and components which can be brought together.
Another thing I noticed is that @reach/dialog
provides a way to determine which element should receive focus when opening the dialog, which can be handy in some cases where you have an extremely long dialog, and the first focusable element would cause it to scroll all the way to the bottom. In a case like this, being able to focus, say, the title instead is a good solution.
@react/reach
is published under MIT, and is now maintained mainly by one person from what I can see on their repository, Chance Strickland. Itâs a big React framework though, so I donât see it disappearing any time soon.
@react-aria
is maintained by Adobe under the Apache-2.0 license, which I assume is less permissive than MIT but still allows commercial use, so thatâs basically just as good for most use cases I guess. There seem to be more contributors, which is kind of nice.
@headlessui/react
is maintained by one person only (as far as I can tell, Robin Malfait) under the MIT license. Itâs part of the TailwindLabs GitHub organization though, so probably not going anywhere.
react-a11y-dialog
and a11y-dialog
are solely maintained by me and published under MIT. I authored a11y-dialog
5 years ago and have published it 50 times across 6 different major releases since then. Thatâs the open-source project I enjoy working on the most, but ultimately, itâs suject to me maintaining it of course.
All that being said, a dialog is a dialog. Once itâs up and running, it should be able to stand the test of time. The accessibility/browser landscape doesnât move so fast that our current implementations will become obsolete a year down the line. Heck, years old implementations are still totally great to this day because nothing really changed that much.
First off: any of these 4 libraries will be great. They are all solving the main problem: creating accessible dialogs. So you canât really go wrong. Iâm not a fan of the absence of sub-packages for @headlessui/react
, and between @reach/dialog
and @react-aria/dialog
, I personally prefer the latter, because itâs half the size of the former but provides the same feature set and flexibility as far as I can tell.
I will keep using react-a11y-dialog
because itâs small, and built on top of a tiny and solid library I maintain myself.
Ultimately, itâs going to depend what you value the most. If you want to do fancy things and want a lot of flexibility, react-a11y-dialog
might fall a little short (although I think youâll still be able to manage to do whatever with it). In a case like this, one of the other libraries would provide you more of a toolbelt to build dialog-based interactions the way you intend it.
If you want something lightweight and simple, react-a11y-dialog
is probably a good idea. Heck, you could even use a11y-dialog instead and build your own component or hook. Ultimately, itâs not much more than:
const MyDialogComponent = props => {
const container = React.useRef()
React.useEffect(() => {
const instance = new A11yDialog(container.current)
return () => {
if (instance) instance.destroy()
}
}, [])
return ReactDOM.createPortal(
<div
ref={container}
id={props.id}
aria-labelledby={props.id + '-title'}
aria-hidden='true'
>
<div data-a11y-dialog-hide></div>
<div role='document'>
<h1 id={props.id + '-title'}>{props.title}</h1>
{props.children}
<button type='button' data-a11y-dialog-hide>
Close
</button>
</div>
</div>,
document.body
)
}
That was a longer answer than I anticipated. I hope this helps anyway! đ
Holy moly, this was an incredible answer, thank you so much for the effort in doing this analysis!
I can assure you that I'll come back to this page many more times when thinking about modals ^.^
incredible discussion. thank you! i'm coming from for you fantastic smashing article... would you mind adding a note on this library in your comparison. sincerely, many thanks. really helpful!!
@dommyboy what smashing article are you referring to? I'm curious now :)
@dommyboy Done. :)
@KittyGiraudel you rock đ€đŒđ€
Gosh, reading one comment here is better then pretty much any tutorial I've stumbled across (besides core a11y documentation of course), but wowâŠwhat an amazingly thorough clarification đŻ đ
Hi @KittyGiraudel and @morkro,
First of all, thank you for creating this accessible dialog, carefully done and documented! Thank you a lot đ
I'm considering replacing a custom dialog/modal implementation in my company's projects with one from the open-source community.
Among many options, this one seems the more solid along with two other candidates that I liked very much:
I'm sure you should be aware of these two libraries. Each one has slightly different implementations / APIs (ways of using), but I believe both have the same features, (or maybe not?).
Could you share your thoughts about those packages and how is this one different from them? Perhaps highlight the differences or the pros/cons?
Thank you đ