Acronis UIKit
Components

Dialog

A modal overlay that interrupts the user to show focused content or request a decision.

Usage

import { Dialog } from '@acronis-platform/ui-react';

Dialog is the sanctioned way to build a dialog. A single variant prop selects one of seven canned use-cases — default, rename, save changes, reset password, discard changes, accept, read-only — each with its own title, body copy, and footer buttons, so product code doesn't hand-assemble the same header/body/footer boilerplate every time. children overrides the body slot; hasLoading shows a spinner overlay across the body + footer. It's built internally on Base UI's Dialog primitive (focus trap, scroll lock, Esc/outside-press dismissal, and ARIA come from Base UI), but that primitive is not itself exported — if none of the seven variants fit your use case, raise it with the design system team rather than reaching for a lower-level API.

An eighth wide variant is a legacy escape hatch — no Figma preset, meant to be paired with a fully custom title/children/footer — kept only for backward compatibility with pre-existing wider dialogs; prefer one of the seven canned variants for new work.

Examples

A canned use-case, controlled open state. onPrimaryAction fires when the primary footer button is clicked — the dialog doesn't close on its own, so close it yourself once the action completes. (read-only is the one exception: its single "Done" button both fires onPrimaryAction and dismisses the dialog.)

const [open, setOpen] = useState(false);

<Dialog
  variant="discard changes"
  open={open}
  onOpenChange={setOpen}
  onPrimaryAction={() => {
    discardChanges();
    setOpen(false);
  }}
/>;

Override the body slot while keeping the header/footer chrome:

<Dialog variant="default" defaultOpen>
  <p>Any custom body content.</p>
</Dialog>

For a full-bleed or fixed-height layout, className styles the popup and bodyClassName styles the body wrapper. The body classes merge with its defaults, so they can remove the default padding and alignment or let it fill the space between the header and footer. Give the body child flex-1 min-h-0 and the individual panes their own overflow when they need independent scroll:

<Dialog
  variant="wide"
  size="large"
  title="Add a widget"
  defaultOpen
  className="h-[90vh] max-w-[min(71rem,calc(100dvw-96px))]"
  bodyClassName="flex-1 min-h-0 p-0 justify-start overflow-hidden"
>
  <div className="flex min-h-0 min-w-0 flex-1">
    <aside
      tabIndex={0}
      className="w-28 shrink-0 overflow-y-auto border-e border-border sm:w-60"
    >
      Categories
    </aside>
    <section tabIndex={0} className="min-h-0 min-w-0 flex-1 overflow-y-auto">
      Widget cards
    </section>
  </div>
</Dialog>

Busy state — the body and footer become inert (unfocusable, unclickable) and the popup gets aria-busy="true" while hasLoading is set; loadingLabel overrides the overlay's accessible name:

<Dialog variant="save changes" hasLoading loadingLabel="Enregistrement…" open />

Localize the canned copy — title, secondaryLabel, primaryLabel, closeLabel, and loadingLabel all override the variant's default English string:

<Dialog
  variant="discard changes"
  title="Modifications non enregistrées"
  secondaryLabel="Retour"
  primaryLabel="Confirmer"
  closeLabel="Fermer"
  open
/>

rename, discard changes, and accept embed a generic "object name" placeholder in their canned title/body. Pass objectName to interpolate the real name (e.g. a file being renamed) instead of overriding title/children by hand:

<Dialog variant="rename" objectName="Q3 Report.xlsx" open onOpenChange={setOpen} />

The rename variant's text field accessible name is also overridable via objectNameLabel (default 'Object name'):

<Dialog variant="rename" objectNameLabel="Nom de l'objet" open onOpenChange={setOpen} />

The popup always keeps a 48px minimum margin from every viewport edge — each size is capped at calc(100dvw - 96px) wide and the popup at calc(100dvh - 96px) tall — so a narrow or short viewport shrinks the dialog rather than letting it run off-screen. Within that clamp the header and footer hold their token heights and never compress; the body has its own scroll container. If you set an explicit popup height and want the body to fill the space between the header and footer, add flex-1 min-h-0 through bodyClassName. Avoid adding another vertical scroll container around ordinary prose children — that would create two competing vertical scrollbars. Short body content stays vertically centered, while content taller than the available space scrolls with the top edge always reachable. The popup's width is likewise fixed to whichever size is active, never content-driven — wide, non-wrapping content (e.g. a table that doesn't wrap) scrolls horizontally within the body instead of widening the popup. A component with its own horizontal-only scroll region (e.g. DataTable) composes fine here since it doesn't compete with the body's vertical scroll:

<Dialog variant="read-only" open>
  {longLegalText.map((paragraph) => (
    <p key={paragraph}>{paragraph}</p>
  ))}
</Dialog>

By default the popup is centered horizontally and pinned 48px from the top of the viewport, matching the full-screen product mockups. Set verticalAlign="center" when a flow calls for vertical centering. The popup keeps its content height, capped at calc(100dvh - 96px):

<Dialog variant="default" verticalAlign="center" open onOpenChange={setOpen}>
  <p>This dialog is centered vertically in the viewport.</p>
</Dialog>

When the dialog renders inside an isolated container (e.g. a shadow root), pass a portalContainer so the popup inherits that scope's styles:

<Dialog portalContainer={mountNode}>{/* … */}</Dialog>

keepMounted leaves the popup markup in the DOM while the dialog is closed (hidden, and not exposed as a dialog to assistive tech) instead of unmounting it — reach for it when the body holds state or an expensive child you don't want torn down and rebuilt on every open:

<Dialog keepMounted open={open} onOpenChange={setOpen}>
  <ExpensiveForm />
</Dialog>

Hide the header and/or footer chrome for a body-only dialog. Both default to true; this is beyond the strict Figma contract (all seven canned variants always show both). When the header is hidden, the title still renders off-screen so the dialog keeps an accessible name:

<Dialog hasHeader={false} open>
  <p>Body-only content — no title bar, no close button.</p>
</Dialog>

A dialog can open another dialog from its own content — render the nested Dialog anywhere inside the parent's React tree (it portals out, so the DOM is unaffected). Base UI flags the inner dialog as nested and deliberately suppresses its backdrop, so stacked dialogs don't darken progressively; to compensate, the parent popup visually recedes while the nested one is open — it scales down slightly and takes a dim scrim derived from the backdrop token, giving the stack a visible depth cue instead of two popups sitting flush against each other. In dark theme the nested popup additionally gains the Dialog tier's hairline border as an edge stroke, since the dark container sits at the black floor and dimming can't differentiate layers there. No extra props are needed:

<Dialog variant="default" size="large" title="Edit step" open>
  <div>
    <p>Parent content.</p>
    <Button onClick={() => setConfirmOpen(true)}>Discard changes…</Button>
    <Dialog
      variant="discard changes"
      objectName="step 2"
      open={confirmOpen}
      onOpenChange={setConfirmOpen}
    />
  </div>
</Dialog>

Legacy wide variant — a free-form footer for pre-existing wider dialogs that don't fit one of the seven canned use-cases:

import { Button, DialogClose } from '@acronis-platform/ui-react';

<Dialog
  variant="wide"
  size="large"
  title="Configure discovery agent"
  open
  footer={
    <>
      <DialogClose render={<Button variant="ghost">Cancel</Button>} />
      <Button>Configure</Button>
    </>
  }
>
  <p>Any custom body content.</p>
</Dialog>

API Reference

Prop

Type

Edit on GitHub

On this page