---
title: "Blueprint: Dialog"
slug: "blueprint-dialog"
category: "frontend"
tags: ["frontend", "blueprint", "react", "dialog", "overlay", "focus-trap", "accessibility"]
status: "stable"
last_updated: 2026-05-14
summary: "Use Blueprint Dialog for modal flows; wrap the app in OverlaysProvider to control portal mounting; always set a title for screen readers."
related: ["[[frontend/blueprint]]", "[[frontend/blueprint-overlays]]", "[[frontend/blueprint-theming]]", "[[frontend/blueprint-table]]", "[[frontend/react]]", "[[frontend/accessibility]]", "[[frontend/forms]]"]
---

## Overview

Blueprint's `Dialog` component is a portal-rendered modal with a built-in focus trap, `Esc` dismissal, backdrop click handling, and ARIA roles. It targets the same use cases as browser-native `<dialog>`: confirmations, detail views, multi-step flows, and form-in-modal patterns. The key decisions are where to mount the portal, how to manage open state, and how to keep focus behavior correct when dialogs are nested or paired with a form library.

## Wrap the app in `OverlaysProvider` for predictable portal mounting

By default, Blueprint overlays (Dialog, Popover, Tooltip, Toaster) mount onto `document.body`. That works for most apps. When a parent applies `transform`, `filter`, or `contain: layout`, it creates a stacking context that clips portaled children or breaks `position: fixed`.

`OverlaysProvider` lets you declare the portal target explicitly and avoids this class of bug proactively.

```tsx
import { OverlaysProvider } from "@blueprintjs/core";

function App() {
  return (
    <OverlaysProvider>
      <Router />
    </OverlaysProvider>
  );
}
```

Mount `OverlaysProvider` once near the root, outside any element that applies CSS containment or a transform. See [[frontend/blueprint-overlays]] for the z-index layering rules that follow from this.

## Control open state in the parent; never inside the Dialog

Dialog is a controlled component. Manage `isOpen` with `useState` in the component that owns the trigger. Blueprint does not have an uncontrolled dialog that manages its own open state.

```tsx
function RecordPanel({ record }: { record: Record }) {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <>
      <Button onClick={() => setIsOpen(true)}>Edit</Button>
      <Dialog
        isOpen={isOpen}
        onClose={() => setIsOpen(false)}
        title="Edit record"
      >
        <DialogBody>
          <RecordForm record={record} onSave={() => setIsOpen(false)} />
        </DialogBody>
      </Dialog>
    </>
  );
}
```

Pass `onClose` to handle `Esc` and backdrop clicks. If either is omitted, those dismissal paths stop working. When using a `MultistepDialog`, pass `onClose` at the top level; individual steps do not need separate close handlers.

## Always provide a `title` prop; never omit it

The `title` prop renders a visible heading and sets `aria-labelledby` on the dialog element. Screen readers announce the title when the dialog opens. Without a title, the dialog has no accessible name and screen reader users have no context for what opened.

```tsx
<Dialog
  isOpen={isOpen}
  onClose={() => setIsOpen(false)}
  title="Confirm account deletion"
  icon="warning-sign"
>
  <DialogBody>
    <p>This action is permanent and cannot be undone.</p>
  </DialogBody>
  <DialogFooter
    actions={
      <>
        <Button onClick={() => setIsOpen(false)}>Cancel</Button>
        <Button intent={Intent.DANGER} onClick={handleDelete}>
          Delete account
        </Button>
      </>
    }
  />
</Dialog>
```

Use `DialogBody` for the scrollable content area and `DialogFooter` for action buttons. Both sub-components apply the correct padding and the `DialogFooter` right-aligns actions by default. Do not nest raw `<div>` elements in place of these sub-components; the layout will break in multi-step dialogs.

## Use `MultistepDialog` for wizard flows

A multi-step dialog keeps the user in a single modal through a sequence of steps. Blueprint's `MultistepDialog` manages navigation, the back/next buttons, and step state.

```tsx
<MultistepDialog
  isOpen={isOpen}
  onClose={() => setIsOpen(false)}
  title="Set up workspace"
  finalButtonProps={{ text: "Finish", onClick: handleFinish }}
>
  <DialogStep id="profile" title="Profile" panel={<ProfileStep />} />
  <DialogStep id="team" title="Team" panel={<TeamStep />} />
  <DialogStep id="plan" title="Plan" panel={<PlanStep />} />
</MultistepDialog>
```

Each `DialogStep` renders its `panel` only when active. Do not conditionally render your own step panels inside a plain `Dialog`; `MultistepDialog` handles keyboard navigation and the step indicator. See [[frontend/forms]] for form validation inside each step.

## Avoid mixing Blueprint Dialog with a Radix-based overlay in the same surface

Radix and Blueprint each maintain a focus-trap and an overlay stack. Nesting one inside the other produces two competing focus traps. The inner dialog may trap focus correctly, but the outer one may not restore focus when the inner closes.

If your app uses [[frontend/shadcn]] components elsewhere, keep the overlay systems separate by surface. Use Blueprint dialogs for the Blueprint-rendered shell (dashboard, data tables) and shadcn dialogs for the shadcn-rendered shell (marketing page, settings form). Never render a Blueprint `Dialog` as a child of a Radix `Dialog.Content` or vice versa. See [[frontend/blueprint-overlays]] for the full overlay layering guidance.

## Test keyboard navigation after every dialog change

Blueprint's Dialog implements the ARIA dialog pattern. The focus trap ensures that Tab and Shift+Tab cycle through focusable elements inside the dialog. `Esc` closes it. The trigger button regains focus on close.

Verify these behaviors after any structural change to the dialog content:

- Tab cycles through all interactive elements in the dialog body and footer.
- `Esc` closes the dialog and returns focus to the trigger.
- Clicking the backdrop closes the dialog.
- Screen reader announces the title on open.

A missing `tabIndex`, a `pointer-events: none` style on a button, or a `display: none` on content can break the focus cycle without a visual sign. See [[frontend/accessibility]] for the full keyboard test checklist.

## Related

- [[frontend/blueprint]]
- [[frontend/blueprint-overlays]]
- [[frontend/blueprint-theming]]
- [[frontend/blueprint-table]]
- [[frontend/react]]
- [[frontend/accessibility]]
- [[frontend/forms]]
