Skip to content

Modal component

Modal is a focus-trapping, portal-rendered dialog for content that needs the user's full attention before they continue — confirmations, forms that block the underlying page, detail views opened from a list. It's fully controlled: you own the open/closed state, Modal just renders it correctly and safely.

Feedback26 propsAccessibility notes

Playground

Change a prop on the right and the preview updates. Switch to Code to copy what you see.

Recipes

Copy-paste patterns for the ways Modal actually gets used — each one a real, self-contained example.

Confirmation dialog

Modal is fully controlled — you own the open state. title is the header, children the body, actions the footer. onClose fires from every dismissal path.

const [isOpen, setIsOpen] = useState(false);

<Button onPress={() => setIsOpen(true)}>Open</Button>
<Modal
  isOpen={isOpen}
  onClose={() => setIsOpen(false)}
  title="Publish changes"
  actions={
    <>
      <Button variant="soft" onPress={() => setIsOpen(false)}>Cancel</Button>
      <Button color="primary" onPress={() => setIsOpen(false)}>Publish</Button>
    </>
  }
>
  Your changes will go live immediately.
</Modal>

Destructive confirm

For a decision the user must make, set role="alertdialog" so screen readers announce it assertively, and lead with a danger action.

<Modal
  isOpen={isOpen}
  onClose={() => setIsOpen(false)}
  role="alertdialog"
  title="Delete project"
  actions={
    <>
      <Button variant="soft" onPress={() => setIsOpen(false)}>Cancel</Button>
      <Button color="danger" onPress={handleDelete}>Delete</Button>
    </>
  }
>
  This can't be undone. All data will be permanently removed.
</Modal>

Sizes

size takes the token scale plus "full" for edge-to-edge — or a raw width. Pick by how much the content needs.

<Modal isOpen={isOpen} onClose={onClose} size="sm" title="Small">…</Modal>
<Modal isOpen={isOpen} onClose={onClose} size="lg" title="Large">…</Modal>
<Modal isOpen={isOpen} onClose={onClose} size="full" title="Full screen">…</Modal>

Block accidental dismissal

For a multi-step or destructive flow, set closeOnOutsideClick={false} so a stray click on the backdrop can't lose the user's progress.

<Modal
  isOpen={isOpen}
  onClose={() => setIsOpen(false)}
  closeOnOutsideClick={false}
  title="Complete setup"
>
  Finish these steps before closing.
</Modal>

Controlled open state

isOpen and onClose are both required — Modal never manages its own visibility. onClose fires from every dismissal path (backdrop click, Escape, the header's close button) so there's exactly one place to update your state, regardless of how the user closed it.

const DeleteConfirm = () => {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <Button color="danger" onPress={() => setIsOpen(true)}>Delete item</Button>
      <Modal
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        title="Delete item"
        actions={
          <>
            <Button variant="soft" onPress={() => setIsOpen(false)}>Cancel</Button>
            <Button color="danger" onPress={handleDelete}>Delete</Button>
          </>
        }
      >
        This can't be undone. Are you sure?
      </Modal>
    </>
  );
};

title renders the header (built on PageTitle internally, with a close button next to it unless dismissible={false}), children is the body, and actions is the footer — typically Cancel/Confirm-style buttons. All three are optional independently, so a Modal can be body-only, or header+body with no footer.

Safe stacking

Opening a second Modal (or a Drawer, since they share the same overlay stack) while one is already open works correctly out of the box — z-index and backdrop layering are computed automatically, and Escape only ever closes the topmost overlay, not every open one at once.

// Nested confirm-inside-a-modal -- stacks correctly without manual z-index math
<Modal isOpen={editOpen} onClose={() => setEditOpen(false)} title="Edit profile">
  <ProfileForm />
  <Modal
    isOpen={confirmDiscard}
    onClose={() => setConfirmDiscard(false)}
    title="Discard changes?"
    role="alertdialog"
  >
    Unsaved changes will be lost.
  </Modal>
</Modal>

Sizing and animation

size accepts the usual token scale plus "full" for an edge-to-edge modal, or a raw string/number for an exact width. animationType controls how it enters/exits (fade, zoom, or a directional slide) with animationDuration/easing to fine-tune the feel; blur and backgroundOpacity control the backdrop underneath it.

<Modal isOpen={isOpen} onClose={onClose} size="lg" animationType="slideUp" title="Details">
  {content}
</Modal>

Confirmation dialogs: role="alertdialog"

For a modal whose sole purpose is a decision the user must make (not just informational content), set role="alertdialog" instead of the default "dialog" — screen readers announce it more assertively, matching the native alertdialog semantics.

Props

The full public API for Modal. Every prop is stable and intentionally typed.

Content

title

ReactNode

default: —

Header title.

children

ReactNode

default: —

Body content.

actions

ReactNode

default: —

Footer action buttons.

Appearance

size

"xs" | "sm" | "md" | "lg" | "xl" | "full" | string | number

default: md

Modal width.

blur

boolean | "sm" | "md" | "lg" | number

default: —

Backdrop blur amount.

backgroundOpacity

number

default: —

Backdrop opacity (0-100).

Layout & positioning

portal

boolean

default: true

Renders through a React portal.

container

Element | DocumentFragment | null

default: —

Custom portal mount target.

zIndex

number

default: —

Explicit stacking z-index; auto-computed when unset.

Behavior & states

isOpen

boolean

default: —

Controls whether the modal is open (required).

role

"dialog" | "alertdialog"

default: dialog

ARIA role.

dismissible

boolean

default: true

Shows a close button in the header.

closeLabel

string

default: "Close modal"

Accessible label for the close button.

closeOnOutsideClick

boolean

default: true

Closes on backdrop click.

closeOnEscape

boolean

default: true

Closes on Escape.

initialFocusRef

RefObject<HTMLElement | null>

default: —

Element to focus when the modal opens.

Motion

animationDuration

number

default: —

Enter/exit animation duration in seconds.

animationType

"fade" | "zoom" | "slideUp" | "slideDown" | "slideLeft" | "slideRight"

default: —

Backdrop entrance/exit animation.

easing

"ease" | "easeIn" | "easeOut" | "easeInOut"

default: —

Animation easing curve.

Events

onClose

() => void

default: —

Called to close the modal (required).

Styling

backdrop

Partial<BackdropProps>

default: —

Extra props forwarded to the underlying Backdrop.

headers

Partial<PageTitleProps>

default: —

Extra props forwarded to the header's PageTitle.

divider

Partial<DividerProps>

default: —

Extra props for the header/body divider.

classNames

Partial<Record<ModalSlot, string>>

default: —

Per-slot class overrides (base/header/headerTitle/body/footer/closeButton).

className

string

default: —

Class applied to the root modal element.

style

CSSProperties

default: —

Inline styles for the root element.

Accessibility

  • Traps focus inside the modal while open, and restores focus to whatever triggered it on close — a keyboard user never gets dropped behind the modal into the rest of the page.
  • closeOnEscape (on by default) closes the modal on Escape; when overlays are stacked, Escape only ever closes the topmost one.
  • initialFocusRef lets you send initial focus somewhere other than the default (the modal itself, or its first focusable element) — useful when a specific field should be focused immediately, like a text input in a form modal.
  • The header's close button has an accessible name via closeLabel (defaults to "Close modal") — override it if the default doesn't read naturally for a specific modal's content.

Best practices

  • Use role="alertdialog" for confirmations and destructive actions, and leave the default role="dialog" for general content modals.
  • Set closeOnOutsideClick={false} for a modal representing a multi-step or destructive flow the user shouldn't be able to dismiss by an accidental click outside it.
  • Keep actions to one or two buttons — a modal footer with many actions usually means the content itself should be a full page instead.
  • For content that slides in from an edge rather than appearing centered (a filters panel, a details sidebar), reach for Drawer instead — it shares Modal's focus-trapping and stacking behavior.
  • Don't nest more than two modals deep in practice, even though stacking is supported — each additional layer makes it harder for the user to track their way back out.