Skip to content
v2.5.0
English
Русский
Documentation

Dialog

Dialog is a compound component for modal overlays with root, trigger, and content parts.
dialog.tsx · 27 lines
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
Tested in
56 tests

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

  • DialogProps
  • DialogTriggerProps
  • DialogContentProps
  • DialogCloseProps

Minimal Composition

minimal-composition.tsx · 53 lines
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 provider
  • DialogTrigger: open action surface
  • DialogContent: modal content container

Optional Parts

  • DialogClose: closes the dialog when clicked; wraps your own button with asChild

Your Own Markup

  • a heading whose id is passed to DialogContent as aria-labelledby (or aria-label on DialogContent); the dialog must have a name
  • action buttons that answer the dialog (Cancel, Confirm) inside a <form method="dialog">

Composition Order

Typical composition:

  1. Dialog
  2. DialogTrigger
  3. DialogContent with your heading, an optional DialogClose, 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 row
  • DialogTitle → a Heading with an id, referenced by aria-labelledby on DialogContent
  • DialogCloseButton → DialogClose asChild around your own button, for example an IconButton with an aria-label
  • useDialogContext() no longer returns contentId or titleId, and DialogTrigger no longer sets aria-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-labelledby or aria-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 DialogContent without aria-labelledby or aria-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.

confirm-action.tsx · 88 lines
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.

controlled-open-state.tsx · 94 lines
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.

unsaved-changes-guard.tsx · 112 lines
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.

form-in-modal.tsx · 100 lines
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.

server-validation-error.tsx · 118 lines
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.

type-to-confirm.tsx · 77 lines
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.

scrollable-content-dialog.tsx · 76 lines
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.

opened-from-popover-menu.tsx · 88 lines
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.

nested-confirm-step.tsx · 101 lines
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.

blocking-dialog.tsx · 41 lines
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: asChild with Button
  • 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; with asChild it renders your own button instead
  • Calls onChangeOpen(false) before your onClick, 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-labelledby on DialogContent or aria-label on DialogContent
  • Typical usage: Heading with an id

Action buttons

Buttons that answer the dialog, such as Cancel and Confirm.

  • Put them in a <form method="dialog">; submitting it closes the dialog through onChangeOpen(false), and the submit button's value becomes the dialog's returnValue
  • Use DialogClose for a standalone close button, such as the X in a header

Composition Order

Typical composition:

  1. Dialog
  2. DialogTrigger
  3. DialogContent
  4. Your heading, an optional DialogClose, body, and a <form method="dialog"> with action buttons

Valid Composition Patterns

minimal-composition.tsx · 53 lines
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.