Skip to content

Instantly share code, notes, and snippets.

@ryanflorence
Last active March 21, 2023 08:45
Show Gist options
  • Save ryanflorence/fd7e987c832cc4efaa56 to your computer and use it in GitHub Desktop.
Save ryanflorence/fd7e987c832cc4efaa56 to your computer and use it in GitHub Desktop.

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.

Focus Management

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.

Scoped Tab Navigation

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

Rendering as a direct child of document.body

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.

Declarative API

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 :)

CSS

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.

Thanks for listening!

That's it, I hope this was helpful, I have the best of intentions writing this up.

@svicalifornia
Copy link

What about nested modals? These happen a lot in enterprise apps: a client will request the ability to create a new folder when saving something, or create a new item when choosing items to associate, or open a color picker when setting other item attributes in a modal, or... you get the idea. These dialogs to be reusable and stackable in a variety of orders, but how would you do that if they're all in the document body to begin with? Toggling visibility is not enough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment