# A modal that respects focus · Components

<!-- https://learn-ui.com/chapters/components/modal -->

Kathryn Gonzalez led design infrastructure at DoorDash, and in November 2017 she wrote [How to Build a Modal](https://www.ryngonzalez.com/blog/how-to-build-a-modal): “While building a modal can seem simple from the start, there are a lot of cases and issues to discover once you release and iterate on it.” Three stories about one modal taught her team about z-index management, accessibility obligations, and a gap in testing.

### Stacking contexts, not z-index

First, a support ticket: event sign-ups collapsed on desktop only, days after a coworker moved an RSVP flow into the new modal. On mobile the guest-count dropdown was a native select; on desktop the custom dropdown rendered behind the modal.

She quotes MDN on stacking contexts, “a three-dimensional conceptualization of HTML elements along an imaginary z-index relative to the user.” A child in one context cannot out-stack a child in another, whatever z-index you give it.

Her takeaway: create new stacking contexts for application content and for modal content, so z-index changes in either stay contained. At DoorDash a LayerManager component, built on react-gateway, renders modal content into its own stacking context so the modal is guaranteed to sit above the app.

### Focus goes in and comes back

Six months later an ADA audit found keyboard accessibility “pretty bad,” chiefly because opening a modal did not move focus into it. She condenses MDN’s dialog guidance: at least one focusable control; focus moves to the default control on open; focus returns to where it was on dismiss; and the tab order wraps inside the dialog. role=“dialog” gives you none of this; you implement it or a library does.

Hence a **focus trap** (In Kathryn Gonzalez's description, focus-trap is a small library that will manage saving the last focused element, finding and focusing on the first focus-able element of the modal, keeping focus within the modal, and restoring focus to the last focused element once the modal is unmounted.). focus-trap “will manage saving the last focused element, finding and focusing on the first focus-able element of the modal, keeping focus within the modal, and restoring focus to the last focused element once the modal is unmounted.” DoorDash uses focus-trap-react inside its modal; re-implementing this per feature is a path to madness and missed edge cases.

> Figure. JavaScriptKathryn Gonzalez, How to Build a Modal (focus-trap example)

Source

The four focus requirements are MDN’s dialog-role guidance as quoted in the post. Kathryn adds that the WAI-ARIA documentation and examples “are your friend.”

### Scroll lock and the real viewport

Third, two tickets about scrolling: the page behind the modal kept scrolling, and on mobile a confirm button fixed to the bottom hid behind the browser’s controls. Viewport-height units are not the visible area on mobile; she cites a survey of URL bar behaviour and a Safari contributor explaining that iOS uses the larger size because relayout during scroll at 60 FPS is not practical. Her fix was a calc for the pixels Safari hides.

And **scroll lock** (Preventing the page behind a modal from scrolling; Kathryn Gonzalez found overflow: hidden insufficient on iOS, where the reliable options are position: fixed on the body or preventing touchmove.) is not overflow: hidden: “On iOS, there are only two reliable ways of doing this: Setting position: fixed on the document.body, or adding event listeners that prevent the default behavior of touchmove on the body content.” They chose position: fixed. The team had tested mobile only in Chrome’s responsive mode. “Always test with real devices.”

### Four takeaways and the motion

Her takeaways: consider accessibility from the start and test with screen readers; know the content the modal will hold; “Design systems, for all they are in technology, are also about processes”; and make the component a tool to distribute best practices, so scroll locking, focus, and mobile behaviour are solved once.

She does not discuss how the modal moves, and Emil Kowalski fills that in. His duration table puts modals and drawers at 200 to 300 ms, and his [skill file](https://github.com/emilkowalski/skill/blob/main/skills/emil-design-eng/SKILL.md) exempts modals from origin-aware scaling: “modals should keep transform-origin: center because they are not anchored to a specific trigger.” Both are in [Practical tips](https://learn-ui.com/chapters/motion/practical-tips).

> Figure. CSSIllustration
