Dialog
A modal window that interrupts the page for a focused task or decision.
Installation
- Install the package.
npm install @hashintel/ds-components- Import the component.
import { useState } from "react";
import { Button, Dialog } from "@hashintel/ds-components";
export const InviteTeammateButton = () => {
const [open, setOpen] = useState(false);
return (
<>
<Button tone="brand" onClick={() => setOpen(true)}>
Invite teammate
</Button>
{open ? (
<Dialog size="sm" onClose={() => setOpen(false)}>
<Dialog.Header title="Invite teammate" />
<Dialog.Body>
<p>Enter the email address of the person to invite.</p>
</Dialog.Body>
</Dialog>
) : null}
</>
);
};When to use
- Do: Use Dialog for a short, focused task the user must finish or abandon before returning to the page — editing settings, inviting a teammate.
- Do: Use Dialog to confirm destructive or irreversible actions, such as deleting an entity type.
- Do: Use
size="fullScreen"for immersive tasks that need the whole viewport, such as mapping the columns of an import.
When not to use
- Don't: Don't use Dialog for supporting content the user consults alongside the page — use Drawer, which keeps the page visible.
- Don't: Don't use Dialog for lightweight content anchored to the control that opened it — use Popover, or Menu for a list of actions.
- Don't: Don't put long or recurring workflows in a dialog — give them a page of their own, so they are linkable and survive a refresh.
- Don't: Don't use Dialog for status the user didn't just request — use Banner.
Anatomy
<Dialog onClose={onClose}>
<Dialog.Header
title="Title"
description="One line of context"
iconName="gear"
actions={headerActions}
/>
<Dialog.Body>Content</Dialog.Body>
<Dialog.Footer actions={primaryActions} secondaryActions={startActions} />
</Dialog>Dialog.Body is required; Dialog.Header and Dialog.Footer are optional, and the types enforce the header–body–footer order. A close button renders automatically — inside the header, or alone when there is no header — unless shouldCloseOn="none".
Examples
Basic
There is no open prop: render the dialog to open it, and remove it when onClose fires. onClose is called when the user dismisses it with Escape, the close button, or a click on the backdrop.
Loading
Set loading while content loads or a request runs: a spinner overlay covers the body, the dialog announces busy, and body scrolling locks. The overlay covers only the body — disable or set loading on footer actions yourself.
Sizes
Six presets cap the width: xs for confirmations, sm for single-field tasks, md (the default) for most forms, lg and xl for data-heavy content, and fullScreen for tasks that need the whole viewport.
Variants
partitionedFooter (the default) seats the footer on the dialog frame, below the rounded content card, so actions read as separate from the content. plain keeps header, body and footer on one continuous panel.
Header
title labels the dialog, description adds one line of context, iconName marks the task, and actions places supporting buttons beside the close button. Give header actions autoFocus="never" so initial focus skips the chrome and lands on the task.
Footer actions
actions renders right-aligned with the primary action last; secondaryActions sits apart on the left — reserve it for destructive or rarely used actions so they can't be pressed by reflex.
Confirming a destructive action
Use an xs dialog whose body spells out what is affected, with an error-tone primary action that names what it deletes — never "OK" or "Yes".
Custom header and footer
Pass children to Dialog.Header or Dialog.Footer instead of the structured props for full control of a region. The close button still renders beside a custom header.
Body without padding
Set withPadding={false} when the body content manages its own layout edge-to-edge — tables, lists, or full-bleed imagery.
Controlling dismissal
shouldCloseOn sets the light-dismiss affordances. closeButtonAndOverlay (the default) closes on the close button, Escape keypress, and backdrop clicks; closeButton drops backdrop clicks — use it when a stray click would discard work; none is the most restrictive and removes the close button and Escape too, so one of your own actions must close the dialog — use it when you require a user response.
Initial and return focus
By default, focus moves to the first focusable element that hasn't opted out via autoFocus="never", and returns to the trigger on close. Point initialFocusRef at the element the task starts with — the Cancel button for destructive confirmations — and returnFocusRef wherever focus should land if the trigger is gone.
Behavior
- While a dialog is open the page behind it can't scroll. The header and footer stay pinned, and scroll applies just to the dialog body.
- Mounting a second dialog while one is open stacks it. Try to keep stacks no more than two deep — deeper nesting disorients users.
Content
- Write titles verb-first, 1–4 words, sentence case: "Edit workspace", "Delete entity type". No terminal punctuation.
- Match the primary action's label to the title's verb — "Delete property" under a "Delete property" title, never "OK" or "Yes".
- "Cancel" is the established dismiss label; prefer it to "Close" whenever the dialog edits anything.
- Keep the header description to one sentence of context or consequence: "Scheduled runs stop while the flow is archived."
- For destructive actions, state what is affected and how many things in the body — "removed from 3 entity types".
Accessibility
Dialog implements the WAI-ARIA modal dialog pattern.
What Dialog provides
- Renders
role="dialog"with focus trapped inside; while open, the rest of the page is hidden from assistive technology and page scroll is locked. - Wires the header's
titleanddescriptiontoaria-labelledbyandaria-describedbyautomatically. - Moves focus on open to
initialFocusRef, else the first element withautoFocus, else the first focusable element not markedautoFocus="never"(the built-in close button opts out). On close, focus returns to the trigger, or toreturnFocusRefwhen set. - Labels the built-in close button "Close dialog", and
loadingsetsaria-busywith a polite live region over the body.
What you must do
- Give every dialog with a header a
title; when it has none, passaria-labelnaming the task. - With
shouldCloseOn="none", Escape is disabled — a deviation from the APG pattern — so always render a visible, enabled action that closes the dialog. - Give header
actionsbuttonsautoFocus="never"so initial focus starts on the task, not the chrome.
API reference
Dialog
| Prop | Type | Description |
|---|---|---|
| size | "xs" | "sm" | "md" | "lg" | "xl" | "fullScreen" default: "md" | |
| onKeyDown | KeyboardEventHandler<Element> | |
| variant | "partitionedFooter" | "plain" default: "partitionedFooter" | |
| shouldCloseOn | "none" | "closeButtonAndOverlay" | "closeButton" default: "closeButtonAndOverlay" | Which dismissals the dialog offers: closeButtonAndOverlay closes on the close button, Escape, and backdrop clicks; closeButton drops backdrop dismissal; none renders no close button and disables Escape, leaving closing entirely to your own controls. |
| loading | boolean | |
| onClose | (() => void) | |
| initialFocusRef | RefObject<HTMLElement> | Focused when the dialog opens, instead of the default initial focus. |
| returnFocusRef | RefObject<HTMLElement> | Receives focus when the dialog closes, instead of the element focused before it opened. |
Dialog.Header
| Prop | Type |
|---|---|
| title | ReactNode |
| description | ReactNode |
| iconName | "function" | "filter" | "at" | "circle" | … 137 more |
| actions | ReactNode |
Dialog.Body
| Prop | Type | Description |
|---|---|---|
| withPadding | boolean default: true | Turn padding on/off. Used when the body content controls padding itself. defaults to true. If set, you will need to apply useAvoidScrollWidthChange yourself if the content height/width can change. |
Dialog.Footer
| Prop | Type | Description |
|---|---|---|
| children | ReactNode | Replaces the footer's action layout entirely with custom content — mutually exclusive with actions/secondaryActions. |
| actions | ReactNode | The overlay's main actions — confirm, cancel — aligned to the footer's end. |
| secondaryActions | ReactNode | Supporting actions aligned to the footer's start, kept apart from the main ones. |
Styling
The size presets only cap max-width; for a bespoke width, override it via className.
import { css } from "@hashintel/ds-helpers/css";
import { Dialog } from "@hashintel/ds-components";
export const NarrowDialog = ({ onClose }: { onClose: () => void }) => (
<Dialog className={css({ maxWidth: "[480px]" })} onClose={onClose}>
<Dialog.Body>
<p>Content at a custom width.</p>
</Dialog.Body>
</Dialog>
);Was this page helpful?