import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
Icon,
IconButton,
Text,
} from '@vega-ui/react';
import { XIcon } from '@vega-ui/icons';- Browser support
- Widely available
- Chrome 111
- Edge 111
- Firefox 121
- Safari 15.6
- Safari on iOS 15.6
- Bundle size
- <dialog>
- <dialog closedby> — only the closedBy prop depends on it: without it, Escape closes and a backdrop click does not
- :has()
- :modal
- ::backdrop
- Oklab and OkLCh — comes from the design tokens
- @starting-style — only the open and close animation depends on it
- Web animations
- backdrop-filter — covered by a -webkit- fallback, not counted in the minimum
- Individual transform properties
- :focus-visible
- :where()
Summary
Dialog is a compound component for modal overlays with root, trigger, and content parts. DialogClose turns your own button into a close button; headings and layout are plain markup you compose yourself. DialogContent renders a native <dialog> opened with showModal(), so the browser puts it in the top layer, makes the rest of the page inert, and draws the backdrop. It is the strongest general-purpose overlay in the family and should be chosen when modal interruption is intentional.
Imports
import {
Dialog,
DialogTrigger,
DialogContent,
DialogClose,
type DialogProps,
} from '@vega-ui/react';Exported Types
DialogPropsDialogTriggerPropsDialogContentPropsDialogCloseProps
Minimal Composition
import { useId, type CSSProperties } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
Icon,
IconButton,
Text,
} from '@vega-ui/react';Required Parts
Dialog: root state and context providerDialogTrigger: open action surfaceDialogContent: modal content container
Optional Parts
DialogClose: closes the dialog when clicked; wraps your own button withasChild
Your Own Markup
- a heading whose
idis passed toDialogContentasaria-labelledby(oraria-labelonDialogContent); the dialog must have a name - action buttons that answer the dialog (Cancel, Confirm) inside a
<form method="dialog">
Composition Order
Typical composition:
DialogDialogTriggerDialogContentwith your heading, an optionalDialogClose, body, and a<form method="dialog">for action buttons
Variants
- Controlled open state versus trigger-driven usage
- Minimal content-only dialog versus a dialog with a header row and close button
- Fixed content dialog versus scrollable or fluid content dialog
Migrating From 2.x
DialogHeader, DialogTitle, and DialogCloseButton were removed: the library keeps only parts that carry behavior plain markup cannot express. DialogClose is the behavior-only replacement for the close button.
DialogHeader→ your own layout element, for example a flex rowDialogTitle→ aHeadingwith anid, referenced byaria-labelledbyonDialogContentDialogCloseButton→DialogClose asChildaround your own button, for example anIconButtonwith anaria-labeluseDialogContext()no longer returnscontentIdortitleId, andDialogTriggerno longer setsaria-controls
Edge Cases
- Nested overlays should be tested for focus restoration and stacking order.
- Long content should be scrollable without hiding the primary actions.
- The dialog must be named with
aria-labelledbyoraria-label. - Keep dismissal behavior explicit when the dialog contains forms or destructive actions.
- If the content starts reading like a full-page workflow, a drawer, sheet, or routed screen may be clearer than a large dialog.
Common Mistakes
- Leaving
DialogContentwithoutaria-labelledbyoraria-label. - Putting critical page content into a dialog without a usable mobile layout.
- Failing to test dialog behavior inside other overlays.
- Treating long dialog content as static without checking scroll and action visibility.
Examples
Basic
Basic: confirm before deleting a file
The Cancel and Delete buttons live in a <form method="dialog">, so either one closes the dialog and its value becomes the dialog's returnValue. The page reads it in onClose, after the close transition, and only then removes the file; Escape and the close button leave returnValue empty.
import { useId, useState, type CSSProperties } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
Icon,
IconButton,
Text,
} from '@vega-ui/react';Controlled/Stateful
Controlled/Stateful: one dialog for many rows
Each row opens the same controlled dialog with its own invoice. The parent owns both open and the selected row, and the native <dialog> returns focus to the button that opened it.
import { useId, useState, type CSSProperties } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
Heading,
Icon,
IconButton,
Text,
} from '@vega-ui/react';
import { XIcon } from '@vega-ui/icons';Controlled/Stateful: keep the dialog open while there are unsaved changes
Every close request (Escape, the close button, Cancel) goes through onChangeOpen(false). While the note has unsaved edits the parent answers the first request with a warning instead of closing; a second request discards the draft.
import { useId, useState, type CSSProperties } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
HelperText,
Icon,
IconButton,
Label,Form/Integration
Form/Integration: rename form with native validation
A <form method="dialog"> with required and pattern fields: the browser blocks invalid submits, and a valid one runs onSubmit (read with FormData) and then closes the dialog. Cancel is DialogClose around a type="button" so it does not submit the form.
import { useId, useState, type CSSProperties, type FormEvent } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
HelperText,
Icon,
IconButton,
Label,Layout/Overlay
Layout/Overlay: server-side validation error
When the check happens after submit, use a regular form and close the dialog yourself: onSubmit prevents the default, waits for the check and either shows the error linked with aria-describedby and aria-invalid or sets open to false. Try submitting the prefilled address, which is already taken. Editing the field clears the error.
import { useId, useState, type CSSProperties, type FormEvent } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
HelperText,
Icon,
IconButton,
Label,Error
Error: destructive action disabled until the name is typed
The Delete button stays disabled until the field matches the workspace name, so neither a click nor Enter in the field can confirm by accident. Cancel is not a submit button, so Enter never triggers it either.
import { useId, useState, type CSSProperties } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
Icon,
IconButton,
Label,
Text,Disabled
Disabled: long scrollable content
Only the list scrolls: it gets a max-height, overflow: auto and overscroll-behavior: contain, while the heading and the Done button stay in place. The scrollable list is focusable and named, so keyboard users can scroll it too.
import { useId, type CSSProperties } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
Icon,
IconButton,
Text,
} from '@vega-ui/react';Disabled: dialog opened from a popover menu
A menu item closes the popover and opens a controlled dialog in the same handler. The menu item no longer exists when the dialog closes, so onClose moves focus back to the menu trigger.
import { useId, useRef, useState, type CSSProperties } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
Heading,
Icon,
IconButton,
Popover,
PopoverContent,
PopoverTrigger,Edge
Edge: nested confirmation step
The member dialog opens a confirmation dialog from inside its content. Escape closes only the top one; confirming closes both, because the handler also sets the outer open to false.
import { useId, useState, type CSSProperties } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
Icon,
IconButton,
Text,
} from '@vega-ui/react';Edge: dialog that closes only through its actions
With closedBy='none' neither Escape nor a backdrop click closes the dialog, so there is no close button either; the only way out is one of the two answers. autoFocus puts focus on the safe answer, and the chosen returnValue is shown below the trigger.
import { useId, useState } from 'react';
import {
Button,
Dialog,
DialogContent,
DialogTrigger,
Heading,
Text,
} from '@vega-ui/react';
export default function BlockingDialog() {
const titleId = useId();Anatomy
Overview
Dialog is a compound component. The root owns open state, focus management, dismissal behavior, and modal coordination across the exported child parts.
Required Parts
Dialog
Root provider for the dialog state.
- Required: yes
- Owns: open state, dismissal, focus lifecycle
- Must wrap every other
Dialog*part
DialogTrigger
User action that opens the dialog.
- Required: effectively yes for normal product flows
- Typical usage:
asChildwithButton - Should stay inside
Dialog
DialogContent
Primary modal surface.
- Required: yes
- Owns: the native modal
<dialog>, its::backdrop, and open and close transitions - Renders into
document.body, so no portal part is needed
Optional Parts
DialogClose
Closes the dialog when clicked.
- Required: no
- Renders a
<button type="button">with no styles, icon, or label; withasChildit renders your own button instead - Calls
onChangeOpen(false)before youronClick, so the close transition plays and a controlled parent can keep the dialog open - Give an icon-only close button an
aria-label
Your Own Markup
The library does not ship header or title parts. They carry no behavior that plain markup cannot express, so you compose them yourself.
Heading
Names the dialog.
- Required: a name is required, either a heading referenced by
aria-labelledbyonDialogContentoraria-labelonDialogContent - Typical usage:
Headingwith anid
Action buttons
Buttons that answer the dialog, such as Cancel and Confirm.
- Put them in a
<form method="dialog">; submitting it closes the dialog throughonChangeOpen(false), and the submit button'svaluebecomes the dialog'sreturnValue - Use
DialogClosefor a standalone close button, such as the X in a header
Composition Order
Typical composition:
DialogDialogTriggerDialogContent- Your heading, an optional
DialogClose, body, and a<form method="dialog">with action buttons
Valid Composition Patterns
import { useId, type CSSProperties } from 'react';
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogTrigger,
Heading,
Icon,
IconButton,
Text,
} from '@vega-ui/react';This is the production-safe default composition for modal flows.
Invalid Composition Patterns
DialogContent outside Dialog
The child parts depend on root context. Rendering them outside Dialog breaks behavior.
Content without a name
DialogContent does not name itself. Without aria-labelledby pointing to your heading or an aria-label, screen readers announce an unnamed dialog.
Nested dialogs without retesting focus flow
Nested overlay stacks can still be valid, but they must be treated as an integration edge case, not assumed to work from isolated component testing alone.