Usage

When a dialog is the right choice, the two ways to open one, and what Modal leaves to you: the footer, the width, the ways out and what happens to the content when it closes.

When to use

A modal holds the page's attention until it is answered. The page behind it is masked, locked from scrolling and out of the tab order, so use one only when that cost is worth paying.
  • One short decision the page cannot go on without: confirm a deletion, pick a destination, name a thing.
  • A form small enough to fit in one view, whose result changes the page behind it.
  • A destructive action that needs one confirmation, with the verb in the button.
What to use instead
  • Drawer when the task is a detour and the page behind it still matters. A drawer keeps the page visible along one edge, so people can read from it while they fill the panel in.
  • Popconfirm for one small question attached to the control that raised it. A whole dialog is heavier than a single yes.
  • A route of its own, or a Result page, when it is a whole task: several steps, a long form, or anything people may want to link to, reload or come back to. A modal has no URL and no back button.
  • A message or a notification when the dialog only reports. Something that needs no answer should not interrupt, so use message or notification.

The element or the function call

There are two ways to open a dialog. Modal is the element: you hold open in state and the dialog is part of your tree. The calls confirm, info, success, error and warning open a dialog from a function.
  • Open confirm dialogs through App.useApp(). Modal does not re-export Modal.confirm and the other static methods. Imported straight from antd, they render outside your provider and come out unthemed. Take modal from App.useApp(), which works anywhere under AntuikitProvider.
  • Modal.useModal() works too, if you would rather hold the context yourself. It returns the calls plus a contextHolder, and the dialogs only appear once you render it.
  • Both return dialogs you can await: a confirm resolves to true on OK and false on Cancel.
  • Closing every dialog at once. Modal.destroyAll(), imported from antd, closes every dialog opened with a function call, including the ones from App.useApp() and Modal.useModal().
  • Choose by what the dialog holds. A one-off question answered in a callback is a function call. Anything with state of its own, like a form, a list or a step, belongs in your tree as an element.
By default the footer shows Cancel, then OK as a primary button (okType defaults to primary). The confirming action sits last, on the trailing edge, and the row flips under direction="rtl".
  • Name the buttons with the verb, not “OK”. okText="Delete project" can be answered without reading back up to the title.
  • footer takes a node to replace the row, or a function (originNode, { OkBtn, CancelBtn }) to rearrange it. The function form adds a third button without rebuilding the other two.
  • Lay out a footer function yourself. The footer row is not a flex container, so an auto margin on an extra button does nothing. Return a Flex from the footer function and place OkBtn and CancelBtn inside it.
  • footer={null} removes the row. Do that only when the content carries its own actions, and check that something in it still closes the dialog.
  • okButtonProps and cancelButtonProps go to the Buttons, which is where a destructive confirm gets its danger and an incomplete form gets its disabled.
  • loading removes the footer while it is on, so a skeleton dialog has nothing to press. You do not need to disable the buttons yourself.

Width and long content

  • width defaults to 520 and takes a number, a CSS length, or one value per breakpoint, like { xs: "90%", md: 720 }.
  • Width stays where you put it. Compact mode shrinks the padding, text and buttons inside a dialog, but width and style keep the values you passed.
  • Let a dialog grow down rather than across. Past about 720px it reads as a page, and a route is usually the better answer.
  • Scroll the body, not the page. Long content scrolls the whole dialog, buttons included. Give styles.body a maxHeight and overflowY: "auto" to keep the title and buttons in view.
  • centered places the dialog in the middle of the viewport instead of near the top. It matters for short dialogs, since a tall one fills the space either way.

Styling the parts

Style the parts with classNames. The dialog renders in a portal on document.body, so selectors written on a parent never reach it. Pass classNames or styles keyed by part: header, body, footer, title, mask and the rest.
className lands on the dialog box, rootClassName on the portal root and wrapClassName on the scroll wrapper.

Closing rules

There are four ways out of a dialog, and all of them call onCancel: the close button, the Cancel button, a click on the mask and Escape. The dialog never closes itself, so onCancel has to set open to false.
  • mask.closable defaults to true (the older top-level maskClosable still works and is deprecated). Turn it off for a dialog with typed input, where a stray click on the page behind would throw the work away.
  • A mask click closes the dialog only when the press and the release both land on the mask, so a text selection that starts inside the dialog and ends on the mask does not close it.
  • keyboard defaults to true and controls Escape. With both it and the mask click off, the footer is the only way out. Keep that for a step that cannot be left half done, and never combine it with closable={false}.
  • A destructive confirm keeps all four ways out and puts the safety in the button: danger on OK, the verb in okText, and the consequence stated in the body.
  • confirmLoading blocks every way out. While it is true, Escape, the mask and Cancel do nothing until you clear it. That is fine for a short request and a problem for a long one.
  • The close glyph is not from your icon pack. Modal draws antd's own close and status icons. Pass closeIcon, or icon on a modal.confirm call, to use your pack's.

loading and confirmLoading

Two kinds of waiting. loading shows a skeleton in place of the body and removes the footer until the content arrives. confirmLoading puts a spinner on OK while a request runs, and Escape, the mask and Cancel do nothing until you clear it.
  • Use confirmLoading for the wait after submit. The content stays on screen, so people can still read what they agreed to.
  • Use loading when the dialog opens before its content is ready, so the box does not appear empty and then jump.
  • Setting both at once usually means one dialog is doing two jobs.
  • Neither one closes the dialog. Clear the flag where you set open to false, or the spinner keeps spinning after the request is done.

destroyOnHidden and what the content remembers

Closed is not unmounted. A closed dialog keeps its children, so a half-filled form is still filled when it opens again. Set destroyOnHidden to start empty each time. It replaces the deprecated destroyOnClose.
  • destroyOnHidden unmounts the content once the close animation ends. Use it for a form that should open blank, and leave it off for a step people come back to.
  • forceRender mounts the content before the first open, for when something outside needs to reach into it. It wins over destroyOnHidden, so passing both keeps the content mounted.
  • afterClose fires once the close animation has finished, and afterOpenChange at the end of both animations. Reset state in afterClose rather than in onCancel, or people watch the content empty itself on the way out.
  • Modal isolates Form and Space context, so a field inside a dialog is not bound to a Form that wraps the dialog's element. Give the dialog its own Form.

Stacking modals

  • Each open dialog gets its own z-index, so a dialog opened from inside another sits above it without a zIndex of your own.
  • Escape reaches only the topmost dialog, so a stack closes one layer at a time. Do not add a key handler of your own for it.
  • The page behind stays locked from scrolling until the last dialog closes.
  • Avoid stacks where you can. Two dialogs deep usually means the first one should have been a page.