4

Accessible Modals with React, TypeScript, and Tailwind

 2 years ago
source link: https://blog.bitsrc.io/simple-accessible-modal-in-react-typescript-and-tailwind-3296704a985a
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Accessible Modals with React, TypeScript, and Tailwind

A dive into portals, hooks, and focus management while creating a simple, accessible modal window.

Why a modal? Modals tend to be unnecessarily bloated, in my experience, which is not ideal since they feature everywhere. Also, it will allow us to (re)visit topics such as portals, accessibility, and hooks.

1*YPpTKwle3GMaX87esY-lqg.png?q=20
simple-accessible-modal-in-react-typescript-and-tailwind-3296704a985a
That’s what we’re cooking today 👨‍🍳

I’ll assume you consider React and TypeScript your friends. It helps if you’re familiar with Tailwind, too, but you can infer lots from the class names.

Foundations

Let’s start with a basic version of our modal window.

We need an overlay to dim the background and a container inside to delineate the actual modal. Enclosed in that container, there is a closer button, the head, and the body. Finally, inside them live the actual contents.

Bare-bones version of our modal window.

Not an awful amount of code, right? We can use it as follows:

Since URL is an integral part of any good UI, we control the opening and closing of our modal via the ?modal parameter in the search params. (That’s React Router 6 up there, a wonderful evolution in my mind.)

I’m sure you’ve used modals that allow you to pass headers and footers via attributes such as <Modal footer={<div>...</div>}>. For some reason, I’ve always found that unappealing, unlike the dot-syntax I’m using here.

Naturally, this version (“Basic” in the sandbox) leaves a lot to be desired.

To close, or not to close

It’s a little annoying having to locate that button in the top right corner to close the window. To cut our users’ mouses some slack, we’ll allow them to escape by pressing, well, the Escape key, or by clicking anywhere outside.

When we pass a closeOnEsc attribute to our modal, the first part is as easy as:

Close on pressing escape.

Done! To embark upon the second part of our mission, let’s add another descriptive parameter to our modal. This time, it’s closeOnClickOutside.

Next, we need to start watching for user clicks. To do that, we attach a click listener to the root of the modal, our transparent overlay. It doesn’t even merit its own gist: onClick={closeOnClickOutside ? onOverlayClick : undefined}.

Of course, if onOverlayClick closed the modal every time it fired, it would be closing even when the actual contents were clicked. That would get annoying the first time a user attempted to select text or clicked some action button.

Somehow, we need to distinguish between the inside and the outside.

Once we attach the reference above to our container, the click listener will check whether the clicked element — determined by e.target— is inside or outside that container of ours, and close only on outsider clicks.

Give it a go, it’s much better already (“Simple” in the sandbox).

Accessibility

With regard to functionality, I think we’re decent. But, we’ve given zero consideration to accessibility, and that’s far from alright. Why is that?

Imagine you’re trying to determine the relevant elements on the screen just by looking at the code. You need to traverse the entire DOM paying attention to the nature and style of every single element. If you happen to miss our overlay— much too easy as it’s wedged deep in the DOM— you might interpret the whole situation completely wrong.

In other words, screen readers’ daily bread is a daunting one. For all of them out there, it would be fair to have a go at making their lives easier. That is to say to move in the direction of accessibility.

There are two essential steps to making our modal accessible:

  1. Indicate with code what parts of the screen are in focus and what is hidden (overlaid) at any given time. For this, we’ll use a portal and aria-hidden.
  2. Manage focus properly to help users with navigation. The native focus() and some tabindex digging shall do that trick.

Portals and the Accessibility Tree

Portals provide a way to render children into a DOM node outside their parent. Typical use cases include breaking free from parents with overflow:hidden or funky z-index, but they’re useful for dodging any unwanted CSS baggage from your ancestors.

And, you guessed it, they are indispensable in accessibility lands.

Look at that! We use our custom hook to create a portal div as a direct child of the body. Then, we simply wrap our entire modal in a createPortal call, passing the portal as a second argument.

Why not just insert our modal into that portal we created with vanilla JS? So that events bubble as we’d expect them to.

1*hdZDuBladrK72vzvJtf4fA.jpeg?q=20
simple-accessible-modal-in-react-typescript-and-tailwind-3296704a985a
Look at that, exactly where we wanted it 💪

While this helps with identifying structures, there are no explicit instructions regarding their interpretation. This is where aria-hidden enters the scene.

Whenever something (substantial) is in the DOM but is not visible, we should tag it using aria-hidden="true". That way, it is removed from the accessibility tree enabling screen readers to skip the content. If we would really like to emphasise something is visible, we can do so via aria-hidden="false".

In other words, when the modal window opens and our overlay shrouds the world, the #root element needs aria-hidden="true". As a bonus, we can also add aria-hidden="false" to our modal.

Wonderfully easy. As for the focus part of accessibility…

Guiding user’s attention

When you open a new tab, the browser automatically places your cursor in the search bar. The input field your cursor is in has a coloured outline. Its background colour might have changed, too. The cursor is blinking.

All this to attract your attention. While the practice of attracting users’ attention can have all sorts of connotations — if it didn’t, there would be no Center for Humane Technology — it is rather virtuous in this case. The UI is doing its absolute best to predict what you’re after and to help you avoid getting lost on the way.

For us, the guiding of users’ attention consists of three interconnected parts:

  1. On opening the modal, we want the first focusable element — such as input, a, or button — to receive focus.
  2. On closing, we would like to return the focus to the element that had triggered the opening of the modal, typically a button or a link.
  3. On pressing the Tab key with the modal open, we want focus to be trapped inside our window, not allowing elements outside to be selected.

Step #2 is simple, we just need to save a reference to document.activeElement when the modal is being open and then focus() it back on closing.

Steps #1 and #3 both rely on our ability to find elements that can receive focus. These are a, button, all sorts of inputs (input, textarea, select), and finally the somewhat uncommon details. Easy, in other words. How about those Tab presses?

Tab and tabindex

When you press Tab, the next focusable element on the page & in your browser’s UI receives focus. What if you wanted to break the natural order determined by the DOM? tabindex has your back.

All focusable elements have a default value of tabindex=0, but you can use any value up to 32,767. When doing so, tabindex=4 will come right after tabindex=3 and before tabindex=5 (and, of course, before any tabindex=0). In other words, the sorting is somewhat unintuitive: 1, 2, 3, ... , 0, 0, 0 Equal tabindex is resolved by checking against the original DOM order.

It’s also worth mentioning that tabindex gives the ability to receive focus to any element, not just the ones focusable by nature. Should you ever want to remove focusability from an element, use tabindex=-1.

Code already, please!

To get all focusable elements, we borrow parts of the code from this post, making sure to exclude anything with tabindex=-1. We also sort the elements to follow the order prescribed above.

Now, we just need a function to carousel through a given array of elements. Nothing strange about the code below, right?

With these two in our arsenal, we can finally go back to our modal and do what we set out to at the start of this section:

  1. On opening, save a reference to the current active element and focus the first focusable element in our modal. (There’s always at least the closer.)
  2. On closing, return focus to the reference we have saved at the start.
  3. On Tab press, carousel through focusable elements inside the container. Use e.shiftKey to make sure Shift+Tab travels backwards, as it should.

You can play with the final version when you choose “Full” in our sandbox.

Granted, writing your own components often implies higher maintenance costs and poorer documentation compared to using existing ones.

At the same time, however, you can craft them to suit your needs exactly, and they bring about better understanding of the problem space. Today, I hope you’ve learnt something from the realms of accessibility, hooks, focus management, or DOM manipulation.

Apart from these two, I have also been after simplicity. Simple, in my mind, is one of the best kinds of praise there is. Simple might very well be one of the goals in life.

Constructive feedback in particular is intensely welcome, but I’ll be glad for any kind. Have a marvellous day!

(See React’s Accessibilty, Web Accessibility Guidelines, or ARIA Modal Dialog Example by W3 for more on accessibility.)

Build applications differently

OSS Tools like Bit offer a new paradigm for building modern apps.

Instead of developing monolithic projects, you first build independent components. Then, you compose your components together to build as many applications as you like. This isn’t just a faster way to build, it’s also much more scalable and helps to standardize development.

It’s fun, give it a try →

0*swkrdxXMbc0rHVwL?q=20
simple-accessible-modal-in-react-typescript-and-tailwind-3296704a985a
An independent product component: watch the auto-generated dependency graph

Learn more


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK