Hi Zach :D
Modals are funny beasts, usually they are a design cop-out, but that's okay, designers have to make trade-offs too, give 'em a break.
First things first, I'm not sure there is such thing as a "simple" modal that is production ready. Certainly there have been times in my career I tossed out other people's "overly complex solutions" because I simply didn't understand the scope of the problem, and I have always loved it when people who have a branch of experience that I don't take the time to level me up.
If you're going to build one there are a few things you should know, and most of this applies to lots of other "modal-like" interfaces.
When the user opens the modal, likely from a button click, you need to move focus to the modal. Otherwise assistive device users have no clue anything happened. It's the equivalent of a visual design toggling visibility on a hidden element 10,000 pixels down the page in an app without visible scroll bars.
Inversely, when the modal is closed, you need to return the focus to the
element that opened it in the first place, otherwise you've dropped the
assistive device user off at the top of document.body
. Its the
equivalent of a visual design calling location.reload()
when a modal
closes in a browser that doesn't restore scroll position.
Go look at rackt/react-modal, it does this.
When the user opens the modal, you should ensure that tab navigation is scoped to the modal. In other words, if I'm at the last tabbable element in a modal, and I press the tab key, focus should move to the first tabbable element in the modal, it shouldn't allow me to navigate to the content behind the modal with the keyboard. Browsers don't make it easy.
Even trickier, when the user tabs from the browser chrome you need to capture that and go focus the first tabbable element in the modal.
Go look at rackt/react-modal, it does this
Modals should not be rendered in the context of the HTML they are
declared in. Unless you're using position: static
, you can run into
CSS limitations when the modal is rendered inside parents with
position: relative
and the whole design can unpredictably break
without workarounds.
Go look at rackt/react-modal, it does this.
Setting aria-hidden="true"
on the rest of the app
I wrote an article on this here. Short story:
Like scoped tab navigation, you also want to prevent assistive devices
from allowing the user to navigate outside the modal with the virtual
cursor, you're (air quotes) "supposed" to use role=dialog
, but that
puts the screen reader into "forms mode", changing the keyboard
navigation hot keys, and makes most of the modal content invisible to
the user.
So instead of role=dialog
, you set aria-hidden=true
on the rest of
the page content and then allow the modal content to be the only visible
content to a screen reader. This way the user can't navigate outside the
modal (the app is "hidden") and the screen reader doesn't enter forms
mode so all the content in the modal is navigable. This is another
great reason to render the modal content as a direct child of
document.body
, makes it easy to hide the rest of the app.
Go look at rackt/react-modal, it does this.
This isn't modal specific, but its React specific. As soon as you grab a
ref to component instance and start calling methods on it like show
that mutate state, you've lost nearly every benefit of React. Your
app's render method + state no longer represent the UI the user gets.
You now have to think about your app over-time, instead of as a snapshot
in time.
If what the user sees can't be predicted by state + render methods, then it can only be predicted by following all the code paths that lead to non-declared state, which is where we were before React.
Any component that has methods that change what is rendered should be
avoided if you want to get the benefits of React's declarative,
functional paradigm (this is why things like Redux can do time travel,
there is no time travel when you call show()
on a ref because it isn't
declarative, recordable state).
If this part isn't sinking in, its okay, I fought it too, but I'm sure you'll eventually come around :)
As for CSS, I shipped injectCSS()
as a convenience in
rackt/react-modal for initial productivity, I expect most people to add
their own styles and not actually call injectCSS()
.
React Modal is one of my first React components, I would love to spend a few days on it to update the API, I feel like I understand React better and would make a few API changes, but its generally a great component.
That's it, I hope this was helpful, I have the best of intentions writing this up.
So after about the 4th time that you mentioned rackt/react-modal, it dawned on me that you made it! (Duh)
One of my first React tasks was to build a modal component for a project. rackt/react-modal was a huge help for me when I didn't know what the heck I was doing. Thanks for that!
It would be interesting to see what changes you would make with your current knowledge/experience. If you ever get around to updating it, I would be willing to help out. I'm sure I'll have to build more modals down the road, and it would be excellent to have a solid one on hand.
Great writeup, by the way!