Aro Andriamaro

Software Developer

How To Build An Accessible Modal

Accessibility in modal dialog
Generated with AI

Modal window (or dialog box) is one of those classic element composing an interface. Every app or website has a modal. But it’s surprisingly hard to get it right.

Accessibility Concerns About Modals

I used to think that building a modal in React is just a matter of showing a box when a variable is set to true (after a click on a button for example). But there are many things, accessibility related, to consider :

Using Headless Modal Component

You could try to implement a modal from scratch. But I strongly advise you against that (at least for production code).

In React, there are many headless component libraries that help developers build accessible interfaces. Headless components are a type of unstyled components that are fully functional.

You can find below few examples of headless modal libraries

React modal is clearly the most popular out there. But I think it is not the best option, because you can disable some accessibility features needed for a modal.

For the other libraries, important accessibility features are enabled by default. Each of them is a good solution but I have a preference for Reach UI dialog for its ease of use (simple API and component easily style-able).

Reach UI Dialog Quickstart

Installation

Terminal window
yarn add @reach/dialog
# or
npm install @reach/dialog

Implementation

In this example, I use Stitches. But you can use any CSS-in-JS library of your choice. You can also use CSS modules to style DialogOverlay and DialogContent

reach-dialog.ts
import { DialogOverlay, DialogContent } from "@reach/dialog";
import { styled } from "path/to/stitches.config.ts";
export const Overlay = styled(DialogOverlay, {
backgroundColor: "hsl(0 0% 0% / 0.439)",
position: "fixed",
top: 0,
bottom: 0,
left: 0,
right: 0,
display: "grid",
placeItems: "center",
});
export const Content = styled(DialogContent, {
backgroundColor: "white",
padding: "1rem",
minWidth: 300,
borderRadius: 20,
});

Usage

reach-dialog-demo.tsx
import { useState } from "react";
import { Content, Overlay } from "./reach-dialog.ts";
const ReachDialogDemo = () => {
const [isOpen, setIsOpen] = useState(false);
const show = () => setIsOpen(true);
const close = () => setIsOpen(false);
return (
<main>
<h1>Reach Dialog Demo</h1>
<button onClick={show}>Open Dialog</button>
<Overlay onDismiss={close} isOpen={isOpen}>
<Content>
<h2>Reach dialog</h2>
<p>Hello World</p>
<button onClick={close}>Close</button>
</Content>
</Overlay>
</main>
);
};
export default ReachDialogDemo;

What About The Official dialog Element ?

Although major browsers support it recently, there are still accessibility issues that are being discussed. I think, reaching for a solution like headless components is totally valid for now.