Skip to content

Components · Overlays

ResponsiveDialog

A Dialog on wide screens and a bottom sheet on phones, switched in CSS.

@rookcom/monarch-reactComponent

Invite form

Usage examples

The example at the top of the page is the only one for this component.

Purpose

One dialog that suits both screens: centred with a margin on desktop, rising from the bottom edge on a phone where the thumb can reach its actions. Same content, same focus trap, same Escape.

Use when

  • A short form or picker used on both desktop and phones.
  • Public or mobile-first pages where a centred dialog feels cramped on a phone.

Avoid when

  • Desktop-only admin screens: use Dialog.
  • A panel that should always come from a side: use Sheet.
  • A confirmation: use ConfirmDialog.

Design principles

  • One API and one DOM for both layouts; nothing is rendered twice.
  • The layout is chosen by a CSS media query, not by matchMedia in JavaScript, so the server render, the first client render and every later render agree: no hydration mismatch and no flash of the wrong layout.
  • Behaviour never changes with the layout: the same focus trap, Escape, scroll lock and focus return in both.

API reference

import { ResponsiveDialog, ResponsiveDialogClose, ResponsiveDialogContent, ResponsiveDialogTrigger } from '@rookcom/monarch-react';
ExportKindRenderingRoot class
ResponsiveDialogcomponentclient.mn-responsive-dialog
ResponsiveDialogClosecomponentclient.mn-responsive-dialog
ResponsiveDialogContentcomponentclient.mn-responsive-dialog
ResponsiveDialogTriggercomponentclient.mn-responsive-dialog

ResponsiveDialog, ResponsiveDialogTrigger and ResponsiveDialogClose are Dialog, DialogTrigger and DialogClose.

ResponsiveDialogContent is DialogContent with the mn-responsive-dialog class, sheetBelow and a grab handle.

Compose the content with the Dialog parts: DialogHeader, DialogTitle, DialogDescription, DialogBody and DialogFooter.

Generated from the TypeScript declarations of packages/react/src/components/overlays/responsive-dialog.

Properties

Every DialogContent prop is accepted (size, footer, busy, closeOnOutsideClick, escapeGuard, closeLabel) except fullScreenOnPhone.

size applies to the dialog layout only; the sheet always spans the screen width.

ResponsiveDialog

A centred Dialog on wide screens and a bottom sheet on narrow ones, from one API and one DOM. The switch is pure CSS (a media query), not `matchMedia`: the server and the first client render are identical, so there is no hydration mismatch and no flash of the wrong layout. Focus trap, Escape and focus return are the Dialog's in both layouts. Compose it with the Dialog parts: DialogHeader, DialogTitle, DialogDescription, DialogBody and DialogFooter.

PropTypeDescription
childrenReact.ReactNode

ResponsiveDialogClose

PropTypeDescription
asChildboolean
childrenReactNode
classNamestring
disabledboolean
idstring

ResponsiveDialogContent

PropTypeDescription
asChildboolean
busybooleanAn action is in progress: Escape, outside clicks and the built-in close button no longer close the dialog, and the content is `aria-busy`. Disable your own Cancel button too.
childrenReactNode
classNamestring
closeLabelstring | nullAccessible label of the built-in close button. Pass null to hide the button.
closeOnOutsideClickbooleanWhether a click outside the dialog closes it. Default `true`; turn it off for forms with typed values so a stray click does not lose them. Escape and the close button still work.
escapeGuardbooleanIgnore Escape while focus is on a control whose own popup is open (a combobox or menu button with `aria-expanded="true"`), so that press only closes the popup. Default `true`.
footerReactNodeActions rendered in a DialogFooter after the children. The footer stays visible while DialogBody scrolls.
handlebooleanShow the grab handle in the bottom-sheet layout. Decorative; default `true`.
idstring
sheetBelow"sm" | "md"The viewport width below which it becomes a bottom sheet: `sm` (640px, default) or `md` (768px).
size"sm" | "md" | "lg" | "xl"Width preset: 24rem, 32rem (default), 44rem or 60rem, capped at the viewport minus 2rem.

ResponsiveDialogTrigger

PropTypeDescription
asChildboolean
childrenReactNode
classNamestring
disabledboolean
idstring

Variants

PropValuesDescription
sheetBelow'sm''md'Below 640px (sm, default) or 768px (md) it is a bottom sheet. Exposed as data-sheet-below.
size'sm''md''lg''xl'Dialog width on wide screens, as on Dialog.
handle'true''false'Decorative grab handle in the sheet layout; default true.

States

StateExposed asBehaviour
Open / closeddata-stateFrom Radix. The dialog fades and scales in; the sheet slides up.
Busydata-busy, aria-busyNothing dismisses it while an action runs.

Interaction behaviour

  • Opening moves focus inside; Tab and Shift+Tab stay inside; the page does not scroll.
  • Escape, the close button, a ResponsiveDialogClose or an overlay click closes it, and focus returns to the trigger.
  • closeOnOutsideClick={false} keeps forms with typed values open on a stray tap.
  • Rotating or resizing the screen switches the layout immediately, without re-mounting or losing input.

Keyboard support

KeysAction
Enter / SpaceOn the trigger, opens it.
Tab / Shift+TabCycles focus within it.
EscapeCloses it and returns focus to the trigger.

Accessibility

Role
dialog (aria-modal)
Accessible name
DialogTitle; DialogDescription becomes the accessible description.
  • The role and name are the same in both layouts, so screen reader users get one consistent experience.
  • The grab handle is aria-hidden and is not a control.

Responsive behaviour

  • At and above the breakpoint: the centred Dialog, min(size, 100vw - 2rem) wide.
  • Below it: full width, anchored to the bottom, rounded top corners, at most the screen height minus 2rem and the top safe area.
  • The sheet pads for the home bar and side notches (env(safe-area-inset-*)).
  • Swipe-to-dismiss is not implemented; the handle is a visual cue only.

Theme support

  • Uses the Dialog surface, border, overlay and shadow tokens; light, dark and more-contrast themes apply automatically.
  • Both entrance animations use duration tokens, which are 0ms under prefers-reduced-motion.

Do and don't

Do

  • Keep the content short enough to read on a phone.
  • Put actions in DialogFooter so they sit at the bottom of the sheet, within thumb reach.

Don't

  • Do not switch between Dialog and Sheet yourself with matchMedia: it renders the wrong layout on the server and flashes on hydration.
  • Do not rely on the handle for dismissal.
  • Dialog: The wide-screen layout and the parts used inside.
  • Sheet: A panel from any edge, at every width.
  • ConfirmDialog: Confirmations with pending and error states.

Dependencies

KindDepends on
npm packagesreact
Monarch componentsDialog

Testing coverage

  • Opens as a dialog, traps focus, closes on Escape and restores focus; axe passes on the open content.
  • Passes Dialog options through (sheetBelow, size, busy, footer).
  • Renders a decorative, aria-hidden handle, removable with handle={false}.
  • Controlled open state; closeOnOutsideClick={false} keeps it open on an overlay click.
  • Never calls matchMedia; the stylesheet holds the breakpoints.
  • Renders on the server and hydrates with no recoverable errors and no console errors.
  • Renders open on the server without touching browser globals.
CheckCovered
Axe in jsdomYes
Server renderingYes
Browser suitesVisual baselines: gallery/responsive-dialog, overlays/responsive-dialog-light (open, 1280, dialog layout) and overlays/responsive-dialog-light-390 (open, phone, sheet layout); Cross-browser axe: /components/responsive-dialog

Tests: packages/react/src/components/overlays/responsive-dialog