Skip to content

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

NameTypeDefaultDescriptionVersion
showbooleanundefinedControlled visibility.Unreleased
defaultShowbooleanfalseInitial visibility when uncontrolled.Unreleased
onUpdateShow(show: boolean) => voidundefinedCalled with the new visibility whenever the modal wants to open or close.Unreleased
titleReactNodeundefinedThe heading; it also names the dialog.Unreleased
childrenReactNodeundefinedThe body. Plain text also describes the dialog (aria-describedby).Unreleased
footerReactNodeundefinedA row below the body, before the buttons.Unreleased
actionReactNodeundefinedReplaces the positive / negative buttons.Unreleased
positiveTextReactNodeundefinedText of the primary button; it shows only when set.Unreleased
negativeTextReactNodeundefinedText of the secondary button; it shows only when set.Unreleased
positiveButtonPropsPartial<ButtonProps>undefinedExtra props for the positive button, such as type: "error".Unreleased
negativeButtonPropsPartial<ButtonProps>undefinedExtra props for the negative button.Unreleased
onPositiveClick(event: MouseEvent) => boolean | void | Promise<boolean | void>undefinedCalled by the positive button. false keeps it open; a promise shows a spinner.Unreleased
onNegativeClick(event: MouseEvent) => boolean | void | Promise<boolean | void>undefinedCalled by the negative button. false keeps it open.Unreleased
closablebooleantrueShow the × button.Unreleased
onClose() => boolean | void | Promise<boolean | void>undefinedCalled by ×. false keeps it open.Unreleased
rawbooleanfalseDrop 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
maskClosablebooleantrueClose on a click on the mask.Unreleased
onMaskClick(event: MouseEvent) => voidundefinedCalled on a click on the mask, whether or not it closes.Unreleased
closeOnEscbooleantrueClose on Escape.Unreleased
onEsc() => voidundefinedCalled on Escape, whether or not it closes.Unreleased
autoFocusbooleantrueFocus the first focusable element of the body (then the buttons, then ×) on open; off focuses the dialog itself.Unreleased
trapFocusbooleantrueKeep Tab inside and hide the page from assistive tech (aria-modal).Unreleased
blockScrollbooleantrueStop 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
tostring | HTMLElement | falseundefinedWhere to mount; false renders in place. Defaults to ConfigProvider’s portalContainer, then document.body.Unreleased
zIndexnumberundefinedA fixed z-index instead of stacking above the layers opened before it.Unreleased
onAfterEnter() => voidundefinedCalled when the enter animation has finished.Unreleased
onAfterLeave() => voidundefinedCalled 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.