Interface Lab
Back to knowledge
Interaction Responsive Motion

Overlay Focus and Scroll Lock Contract

An overlay is not just a scrim; it is a contract across modality, initial focus, focus containment, dismissal, inert background, scroll locking, portal stacking, and mobile safe areas.

Design Principle

Whenever an overlay changes the interactive scope, keyboard, screen reader, pointer, scroll, and mobile behavior must be governed together; a visually covered but semantically reachable background is the most dangerous fake modal.

Large Theory

Overlay design is about interaction context, not z-index. Modal dialogs, drawers, sheets, command palettes, confirmations, popovers, menus, and tooltips all appear above the page, but their modality differs. A modal makes the background unavailable and keeps focus inside. A non-modal popover provides lighter temporary context. A tooltip should not contain task-critical interactive content. Before choosing a component, decide whether the overlay blocks the main page, then define focus, dismissal, scroll, portal, and mobile behavior.

Small Points

  1. Classify the pattern before choosing a component: modal dialogs/drawers/sheets need inert background, contained focus, and focus return; non-modal popovers/menus need trigger relationships, Escape/outside-click behavior, and keyboard paths; tooltips are not interactive.
  2. Initial focus is not always the first button. Short confirmations can focus a safe cancel or close action, long content can focus the heading or first static node, command palettes can focus search, and destructive confirmations should not auto-focus the dangerous action.
  3. Tab and Shift+Tab in a modal should cycle inside the overlay. On close, focus returns to the trigger; if the trigger no longer exists, it returns to a logical replacement such as a row, page heading, or create button.
  4. Use aria-modal="true" only for real modal overlays: code must prevent all users from interacting with the background, and visual styling must make the background clearly unavailable. Otherwise screen-reader users may lose needed context incorrectly.
  5. <dialog>.showModal() can place the rest of the document into the browser top-layer/inert mechanism. Custom portal, Radix, or headless implementations still need explicit inert, ARIA, focus, Escape, and restoration behavior.
  6. Scroll lock distinguishes body, the app's main scroller, and the overlay's internal scroll area. Locking the background should preserve scroll position, compensate scrollbars, keep long overlay content scrollable, and prevent mobile scroll bleed and scroll chaining.
  7. Dismissal needs a state matrix: close button, Escape, outside click, route change, browser Back, swipe down, and dirty form states each need allow/confirm/block behavior and a focus destination.
  8. Stacking rules must be explicit: nested dialogs, popovers inside dialogs, toasts, sticky headers, portal roots, z-index tokens, and top-layer behavior should be predictable. Avoid incomprehensible stacks of bottom sheets.
  9. Motion must not leave invisible controls focusable. Exit animations need synchronized inert/unmount timing and focus return; reduce spatial motion under prefers-reduced-motion.
  10. Mobile sheets and drawers need safe-area, dynamic viewport, virtual keyboard, fixed bottom action, visible close control, and Back gesture checks. A drag handle alone is not an accessible close mechanism.

Design Judgment

An overlay is healthy when a user can open, understand, operate, and close it with only the keyboard; focus does not escape to the background; screen readers cannot navigate into obscured content; the body does not scroll behind it; and mobile keyboard/safe-area behavior does not hide critical controls. A visual modal whose background remains tabbable, readable, or scrollable is a high-risk bug.

Implementation Notes

Output an OverlayContract: type, modality, triggerRef, initialFocusTarget, focusTrapScope, returnFocusTarget, closeMethods, dirtyClosePolicy, ariaRole, ariaLabelledBy, ariaDescribedBy, inertTargets, portalRoot, zIndexToken, scrollLockTarget, scrollPositionRestore, internalScrollArea, overscrollBehavior, iosTouchPolicy, safeAreaInsets, keyboardViewportPolicy, animationMountPolicy, nestedOverlayPolicy, and testAssertions. In React, prefer Radix Dialog/Popover, React Aria useModalOverlay, or locally verified primitives. Custom implementations need Playwright coverage for Tab, Shift+Tab, Escape, close, return focus, body scroll, and mobile viewport behavior.

Counterexamples/Risks

A visual div modal without role=dialog or <dialog>; a scrim that still leaves background buttons tabbable; closing leaves document.activeElement on body; aria-modal=true while the background remains clickable; long dialog content cannot scroll; body scroll jumps to top; iOS bottom sheets bleed touch to the list below; Escape closes the outer layer instead of the inner layer; a command-palette exit animation leaves a hidden input focusable; and a popover contains a multi-step form with destructive submit.

Case Study

The WAI-ARIA APG modal dialog pattern treats inert background, contained focus, Tab looping, and initial focus placement as one behavior set, and warns that aria-modal should only be used when background interaction is genuinely prevented. HTML/MDN <dialog> and WHATWG inert behavior explain that showModal() places the rest of the document into top-layer/inert semantics. React Aria useModalOverlay treats assistive-technology hiding, focus containment/restoration, and page scroll locking as core overlay responsibilities. Radix Dialog/Popover package modal/non-modal behavior, focus, Title/Description, Escape, and trigger return into primitives. NN/g bottom-sheet research adds mobile nuance: modal sheets block the background, nonmodal sheets keep background interaction, and both need clear dismissal, no stacking, and short task scope. The reusable lesson is that overlay quality comes from the behavior contract, not shadows, radius, or scrim color.

Source Links

WAI-ARIA APG Modal Dialog: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/ MDN dialog element: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog WHATWG HTML inert/dialog top layer: https://html.spec.whatwg.org/dev/interaction.html React Aria useModalOverlay: https://react-aria.adobe.com/Modal/useModalOverlay.html Radix Dialog: https://www.radix-ui.com/primitives/docs/components/dialog Radix Popover: https://www.radix-ui.com/primitives/docs/components/popover Primer Dialog Accessibility: https://primer.style/product/components/dialog/accessibility/ MDN overscroll-behavior: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/overscroll-behavior NN/g Bottom Sheets: https://www.nngroup.com/articles/bottom-sheet/

Agent Directive

Before implementing an overlay, output modality, focus map, close matrix, background inertness, scroll-lock target, portal/z-index, mobile safe-area, and test assertions; a visual scrim does not replace semantics and scroll locking.