Modal
Modal puts a dialog over a dimmed page: a centered card on desktop and a bottom sheet on phones, from one component. It covers both a card and a dialog: title, a body, footer and positive / negative buttons, or raw to draw your own surface.
It is a floating layer like Popover, not a native <dialog>: it stacks with the z-index manager, so a Select, Popover or Tooltip opened inside it shows above it, and Escape closes the innermost layer first. While it is open, focus stays inside, the page behind is hidden from assistive tech and does not scroll, and focus returns to whatever opened it when it closes.
Demos
Basic
show and onUpdateShow control it. Focus moves to the first field of the body; Escape, a click on the mask, ×, Cancel and Save all close it.
Async and cancelled closing
Return a promise from onPositiveClick and the button shows a spinner until it settles. Return false (or a promise of false) to keep the modal open; onClose does the same for ×.
Required choice
maskClosable={false}, closeOnEsc={false} and closable={false} leave only the buttons. onMaskClick and onEsc still report the attempts.
Long content
The title and the buttons stay put; the body scrolls between them. footer adds a row above the buttons.
Dialog and bottom sheet
presentation="auto" (the default) is a dialog that becomes a bottom sheet under 640px, Grid’s s breakpoint. dialog and sheet fix one. A sheet spans the screen, slides up from the bottom, keeps clear of the home indicator and gives the buttons equal width. Set an app-wide default with ConfigProvider’s componentOptions={{ Modal: { presentation: "dialog" } }}.
Raw
raw drops the surface, title row and ×, keeping the mask, placement, motion and focus handling. Name the dialog yourself with aria-label or aria-labelledby.
Layers inside
A Select inside the modal opens above it, and a modal opened from a modal stacks on top: Escape or a click on its mask closes only the top one.
Props
| Name | Type | Default | Description | Version |
|---|---|---|---|---|
show | boolean | undefined | Controlled visibility. | Unreleased |
defaultShow | boolean | false | Initial visibility when uncontrolled. | Unreleased |
onUpdateShow | (show: boolean) => void | undefined | Called with the new visibility whenever the modal wants to open or close. | Unreleased |
title | ReactNode | undefined | The heading; it also names the dialog. | Unreleased |
children | ReactNode | undefined | The body. Plain text also describes the dialog (aria-describedby). | Unreleased |
footer | ReactNode | undefined | A row below the body, before the buttons. | Unreleased |
action | ReactNode | undefined | Replaces the positive / negative buttons. | Unreleased |
positiveText | ReactNode | undefined | Text of the primary button; it shows only when set. | Unreleased |
negativeText | ReactNode | undefined | Text of the secondary button; it shows only when set. | Unreleased |
positiveButtonProps | Partial<ButtonProps> | undefined | Extra props for the positive button, such as type: "error". | Unreleased |
negativeButtonProps | Partial<ButtonProps> | undefined | Extra props for the negative button. | Unreleased |
onPositiveClick | (event: MouseEvent) => boolean | void | Promise<boolean | void> | undefined | Called by the positive button. false keeps it open; a promise shows a spinner. | Unreleased |
onNegativeClick | (event: MouseEvent) => boolean | void | Promise<boolean | void> | undefined | Called by the negative button. false keeps it open. | Unreleased |
closable | boolean | true | Show the × button. | Unreleased |
onClose | () => boolean | void | Promise<boolean | void> | undefined | Called by ×. false keeps it open. | Unreleased |
raw | boolean | false | Drop the surface, title row and × to draw your own. | Unreleased |
presentation | 'dialog' | 'sheet' | 'auto' | 'auto' | Dialog, bottom sheet, or a dialog that becomes a sheet under 640px. | Unreleased |
maskClosable | boolean | true | Close on a click on the mask. | Unreleased |
onMaskClick | (event: MouseEvent) => void | undefined | Called on a click on the mask, whether or not it closes. | Unreleased |
closeOnEsc | boolean | true | Close on Escape. | Unreleased |
onEsc | () => void | undefined | Called on Escape, whether or not it closes. | Unreleased |
autoFocus | boolean | true | Focus the first focusable element of the body (then the buttons, then ×) on open; off focuses the dialog itself. | Unreleased |
trapFocus | boolean | true | Keep Tab inside and hide the page from assistive tech (aria-modal). | Unreleased |
blockScroll | boolean | true | Stop the page from scrolling while open. | Unreleased |
displayDirective | 'if' | 'show' | 'if' | Unmount when closed, or keep the DOM, hidden. | Unreleased |
transformOrigin | 'mouse' | 'center' | 'mouse' | Grow out of the click that opened it, or out of its center. | Unreleased |
to | string | HTMLElement | false | undefined | Where to mount; false renders in place. Defaults to ConfigProvider’s portalContainer, then document.body. | Unreleased |
zIndex | number | undefined | A fixed z-index instead of stacking above the layers opened before it. | Unreleased |
onAfterEnter | () => void | undefined | Called when the enter animation has finished. | Unreleased |
onAfterLeave | () => void | undefined | Called when the leave animation has finished. | Unreleased |
Other props, className, style and ref go to the dialog element (role="dialog"); give it a width with style, which a sheet still stretches to the screen. With reduced motion the dialog and the sheet only fade.