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

Dialog

Dialog is a compound component for modal overlays with root, trigger, and content parts.

Labeling

DialogContent does not name itself. Naming the dialog is your job, and it is required: a dialog without a name is inaccessible, because screen readers announce it only as "dialog".

Recommended pattern:

  • a visible heading with an id, referenced by aria-labelledby on DialogContent
  • aria-label on DialogContent when there is no visible heading
  • aria-describedby pointing to the sentence that explains the consequence, so it is announced together with the name
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';

Keyboard Behavior

AnnouncementFocus the demo and press a key from the table.
KeyAction
EnterSpaceOn the trigger: opens the dialog. The dialog itself takes the focus, or the child with autoFocus.
TabShift + TabMoves the focus to the next or previous control inside the dialog. The page behind is inert, so the focus does not reach it.
EscapeCloses the dialog and returns the focus to the element that opened it. With closedBy='none' it does nothing.
Every row is checked in a real browser: Playwright presses the key in Chromium and checks where the focus and the active item land.

In nested dialogs Escape closes only the top one.

Focus Behavior

  • focus should enter the dialog when it opens
  • closing should restore focus to a sensible trigger
  • nested dialog stacks should preserve predictable focus restoration

Focus handling comes from the native <dialog>: the rest of the page is inert while the dialog is open, and closing restores focus to the element that was focused before opening. In Safari a mouse click does not focus a button, so after a mouse-opened dialog closes focus returns to the page body; keyboard activation returns it to the trigger.

On open the dialog itself takes focus, so nothing inside is highlighted and screen readers announce its title; Tab moves to the first control. Put autoFocus on a child to focus it instead, for example the first field of a form.

Screen Reader Semantics

  • the dialog is exposed as role=dialog with aria-modal by the browser and is named by your aria-labelledby or aria-label
  • the trigger exposes aria-haspopup='dialog' and aria-expanded
  • the body content should make sense as one modal unit
  • close controls should remain explicit when dismissal matters; DialogClose has no default label, so an icon-only button inside it needs an aria-label; the library ships no built-in strings

Form Semantics

When the dialog contains fields, treat it as a normal form boundary and retest keyboard submit, close, and focus-return behavior in the real flow.

Buttons in a <form method="dialog"> close the dialog through onChangeOpen(false), and the submit button's value becomes the dialog's returnValue, as with a native <dialog>. This also works with closedBy='none', where Escape and the backdrop do not close the dialog.

Accessibility Risks

  • opening a dialog without aria-labelledby or aria-label
  • using nested dialogs without retesting focus restoration
  • putting long content into a dialog without a usable scroll strategy
  • relying only on click-away dismissal for important flows