Overview

Blueprint’s overlay family (Dialog, MultistepDialog, Drawer, Popover, Tooltip, ContextMenu, OverlayToaster) renders through React portals and shares one overlay stack that decides which overlay closes on Esc and holds focus. The stack lives in OverlaysProvider. Setup is in blueprint.

Mount one provider at the root

OverlaysProvider holds the global overlay stack; it does not choose where portals mount. PortalProvider sets portalContainer and portalClassName for portal output (on document.body by default). BlueprintProvider combines both with HotkeysProvider.

<BlueprintProvider portalContainer={document.getElementById("overlay-root") ?? undefined}>
  <App />
</BlueprintProvider>

Mount it once, outside any element that applies transform, filter, or contain: layout. Portaled content inherits the dark theme only when the bp6-dark class (or data-bp-color-scheme="dark") is on an ancestor of the portal target, such as <html>; see blueprint-theming.

Control Dialog state in the parent

Dialog is controlled: keep isOpen in the component that owns the trigger and pass onClose, which handles Esc and backdrop clicks. Omit it and neither works.

const [isOpen, setIsOpen] = useState(false);
<Dialog isOpen={isOpen} onClose={() => setIsOpen(false)} title="Edit record">
  <DialogBody><RecordForm onSave={() => setIsOpen(false)} /></DialogBody>
  <DialogFooter actions={<Button intent="primary" onClick={save}>Save</Button>} />
</Dialog>

Always set a dialog title

When title is supplied, Dialog renders it as the heading (an h2 by default) and sets aria-labelledby to it. Without a title, supply aria-labelledby yourself, or the dialog has no accessible name. Use role="alertdialog" for destructive confirmations that need an immediate response. Use DialogBody and DialogFooter rather than raw divs.

Use MultistepDialog for wizards

MultistepDialog manages step state, navigation, and the step indicator. Pass onClose once at the top, give each DialogStep an id, title, and panel, and set finalButtonProps for the last step. Do not hand-build steps in a plain Dialog.

Use Popover for interaction and Tooltip for hints

Popover opens on click and holds interactive content such as a Menu. Tooltip opens on hover or focus and holds text only; it closes on pointer leave, so a button inside it cannot be reached.

<Popover content={<Menu><MenuItem icon="edit" text="Edit" /></Menu>} placement="bottom-start">
  <Button icon="cog" text="Actions" />
</Popover>
<Tooltip content="Download as CSV"><Button icon="download" minimal /></Tooltip>

Await OverlayToaster.create

In Blueprint 6, OverlayToaster.create() returns a Promise; createAsync is deprecated. Create one toaster at startup, export the promise, and await it before show(). Calling create in several places builds several containers.

export const toasterPromise = OverlayToaster.create({ position: "top-right", maxToasts: 5 });
(await toasterPromise).show({ message: "Saved.", intent: "success" });

Do not stop Esc inside overlay content

An overlay closes on Esc through a keydown handler on its container. A child onKeyDown that calls stopPropagation() for Escape blocks it. If a widget such as a combobox needs its own Esc handling, stop propagation only there.

Layer with Blueprint’s stacking values

Overlays use z-index: 20 (.bp6-overlay) and toast containers use 40 in Blueprint 6.21. Place custom floating UI relative to those values; arbitrary values like 9999 leave no room for the next layer.

Keep one overlay library per surface

Do not render a Blueprint Dialog inside a Radix Dialog.Content, or the reverse. Each library manages its own focus trap and stack, so nesting them risks competing traps. Use one system per surface; see shadcn-composition for the Radix side.

Test keyboard behavior after each change

Verify that Tab cycles inside the dialog, Esc closes it and returns focus to the trigger, the backdrop click closes it, and a screen reader announces the title. A pointer-events: none button or display: none content can break the cycle without any visual sign. See accessibility.