Modal

A low-level, accessible overlay primitive. Portals its content, dims the page with a Backdrop, traps focus, locks body scroll, and closes on Escape (top modal only) or backdrop click.

Demos

For usage examples, visit the component demo page: Modal

Import

import { Modal } from '@soroush.tech/design-system/Modal'

Props

PropTypeDefaultDescription
isOpenboolean-If true, the modal is shown.
childrenReactNode-A single content element.
onClose(event, reason: 'escapeKey' | 'backdropClick') => void-Fired on Escape (top modal only) or backdrop click.
hasBackdropbooleantrueRender the dimmed backdrop.
shouldKeepMountedbooleanfalseKeep children mounted while closed.
shouldUsePortalbooleantruePortal the modal into portalContainer.
portalContainerHTMLElement | (() => HTMLElement | null) | nullbodyPortal target, or a function returning one.
shouldLockScrollbooleantrueLock body scroll while open.
shouldAutoFocusbooleantrueMove focus into the modal on open.
shouldTrapFocusbooleantrueTrap Tab focus within the modal.
shouldEnforceFocusbooleantruePull focus back into the modal whenever it escapes.
shouldRestoreFocusbooleantrueRestore focus to the trigger on close.
scroll'paper' | 'body'paperWhere long content scrolls. See below.
layerkeyof Theme['zOrder']modalStacking layer, resolved from theme.zOrder.

The content element should carry its own dialog semantics (role="dialog", aria-modal, a label). Modal's root is role="presentation".


Scrolling long content

When content is too tall for the viewport, scroll controls where it scrolls:

  • scroll="paper" (default) - the root centres the content and it scrolls within the surface. Give your Paper a maxHeight and overflow="auto".
  • scroll="body" - the root itself is the scroll container. The surface keeps its natural height and the whole root (surface plus backdrop) scrolls. Short content is centred; tall content scrolls instead of clipping. Don't cap the surface height - use a margin for breathing room.

Server rendering

The modal and its backdrop are drawn in a Portal, which renders nothing on the server: it is absent from server-rendered and prerendered HTML, and mounts on the client when it opens (on a click, hover, focus or key, or when the app sets it open). One that starts open, by its open prop, is still closed in the static HTML and opens on hydration. Its trigger renders on the server as usual.

Example

const [isOpen, setIsOpen] = useState(false)

<Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
  <Paper role="dialog" aria-modal="true" aria-label="Settings" p={4}>
    <Typography variant="h5">Settings</Typography>
    <Button onClick={() => setIsOpen(false)}>Close</Button>
  </Paper>
</Modal>

Source code

If this page does not answer your question, have a look at the implementation of the component for more detail.