Dialog

Developer Experience (DX)

How developers use the Dialog, with one consistent, typed API that composes with forms and other components and leaves when to open and close it to you.

Next, let's look at how developers use the Dialog. It has one API, with the same names as every other component, it's typed end to end, and it leaves when to open and close it to you.

import { useState } from "react";
import { Button } from "@/components/Button";
import { Dialog } from "@/components/Dialog";
import { Input } from "@/components/Input";

export function EditProfile() {
  const [open, setOpen] = useState(false);

  function save() {
    // …save the profile, then close the dialog.
    setOpen(false);
  }

  return (
    <>
      <Button onClick={() => setOpen(true)}>Edit profile</Button>
      <Dialog
        id="edit-profile"
        title="Edit profile"
        open={open}
        onClose={() => setOpen(false)}
        actions={
          <>
            <Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
            <Button color="primary" onClick={save}>Save changes</Button>
          </>
        }
      >
        <label htmlFor="name">Full name</label>
        <Input id="name" autoComplete="name" />
      </Dialog>
    </>
  );
}

Vanilla and Astro have no state to bind open to, so their script opens the dialog from the button that controls it, and lets the browser close it. The close event's returnValue then says why.

Consistent

Once you've learned the Button's API, you already know most of the Dialog's. These names mean the same thing everywhere in Open Components:

NameKindWhat it means
sizePropHow big the component is, which for a dialog is how wide it gets
openPropWhether it's showing, for anything that opens on top of the page
actionsSlotButtons that act on the component, after everything else
closeEventPeople asked to close it, and why

We recommend size="md" and role="dialog" as the defaults. The Dialog also leaves a few things out, on purpose:

  • There's no variant or color. A dialog has no intent of its own, so its actions say what it's about, like a Delete project Button with color="error".
  • There's no trigger slot. The button that opens a dialog is a Button of yours, with its own label and its own place on the page, and the Dialog gives focus back to it on its own.
  • There's no modal prop. A dialog that leaves the page working, next to what opened it, is a Popover, which is on our Roadmap.
  • There's no closable prop either. Every dialog closes on Escape, and an alert dialog leaves out the close button and ignores the backdrop on its own.

Type-safe

Props, events and slots are typed, so your editor can autocomplete them and your type checker (tsc, vue-tsc, svelte-check, ngc or astro check) catches mistakes like size="xl", which isn't one of the Dialog's sizes, or a dialog without a title.

PropTypeDefaultDescription
openbooleanfalseShows the dialog. It never changes on its own
idstringRequired. Its title's and description's IDs start with it
titlestringRequired. The heading that names the dialog
descriptionstringA sentence or two that screen readers read along with the title
role"dialog" | "alertdialog""dialog"alertdialog for a confirmation that needs an answer
size"sm" | "md" | "lg""md"How wide it gets, at most
closeLabelstring"Close"The close button's label, for other languages
EventPayloadDescription
close"escape" | "close-button" | "backdrop" | "action" | "closed", stringFires when people ask to close it, with why, and for action, the value of the button that sent the <form method="dialog">. Fires with closed when it's already closed

Angular is the one framework where two of these names change, since its Dialog sits on the <dialog> itself. A title attribute stays on the element even when an input reads it, which would show the title as a tooltip over the whole panel, so the title is dialogTitle. close is already the <dialog>'s own event, and an output carries a single value, so the event is closeRequest, with { reason, value }.

SlotDescription
defaultThe content, like a message or a form, which scrolls when it doesn't fit
actionsThe Buttons that answer the dialog, like Cancel and Save, in the order people read them

We also export DialogProps, which comes in handy for confirmations you set up from data:

import type { DialogProps } from "@/components/Dialog";

const confirmations: Record<string, Pick<DialogProps, "id" | "title" | "description">> = {
  delete: { id: "delete-project", title: "Delete project?", description: "You can't undo it." },
  archive: { id: "archive-project", title: "Archive project?", description: "Nobody can change it after." },
};

Composable

The Dialog renders the <dialog> as its root, so everything you pass lands on it, including classes, styles, data attributes and event listeners. Its title and description get their IDs from the dialog's own id, as in edit-profile-title, so you can point at them from elsewhere, like a test.

Forms

A <form method="dialog"> is how HTML answers a dialog. On its own, it closes the dialog and sets returnValue to the value of the button that sent it. The Dialog asks instead, firing close with the reason action and that value, so you decide what happens, the same way as for every other reason. That also means the answer works before your JavaScript loads, in Astro and Vanilla.

<Dialog
  id="delete-project"
  role="alertdialog"
  title="Delete project?"
  open={open}
  onClose={(reason, value) => {
    setOpen(false);
    if (value === "delete") deleteProject();
  }}
  actions={
    <form method="dialog">
      <Button type="submit" value="keep" variant="outline">Keep project</Button>
      <Button type="submit" value="delete" color="error">Delete project</Button>
    </form>
  }
/>

A form in the body works like any other, so a dialog that edits something can send it with a submit Button among its actions, through the button's form attribute, and close once it's saved.

With other components

  • A Button opens the dialog, and gets focus back when it closes. The close button is a Button too, and so are the actions, with their names, focus rings and type="button".
  • An Input in the body gets the focus when the dialog opens, if it's the first control there, and keeps its label and error message as it would anywhere else.
  • A Spinner shows that an action is under way, inside its Button, with loading, which keeps the focus on the Button while the dialog waits.
  • Menus, popovers and tooltips inside a dialog, which are on our Roadmap, open in the top layer too, above the dialog, and close on Escape before it does.

Controllable

The Dialog doesn't keep any state you can't reach. Whether it's open is yours, and so is everything that happens when people ask to close it:

StateHow you control it
Openopen, which it never changes on its own
Closingclose, with why, after which you set open to false, or keep it open, unless it's closed
Initial focusdata-autofocus on the control to start on
Focus afterBack to the opener on its own, or anywhere you move it once open is false
Rolerole, which also decides the close button and the backdrop

DX Checklist

RuleLevelScopeRequirementBasisCheck
dialog/shared-vocabularyMustComponentIt uses the shared names: open, size, actions and close (closeRequest in Angular)Open ComponentsReview
dialog/typedMustComponentProps, events and slots are typed, with unions rather than string, and id and title (dialogTitle in Angular) are requiredOpen ComponentsType check
dialog/controllableMustComponentIt opens and closes with open, and asks rather than closing itself: Escape, the close button and the backdrop fire close, with why, and when a browser closes it anyway, it fires close with closedOpen ComponentsUnit test
dialog/root-attributesMustComponentAttributes, classes and listeners land on the <dialog>Open ComponentsUnit test
dialog/form-actionsShouldComponentA <form method="dialog"> inside it fires close with the reason action and its button's value, rather than closing itHTMLUnit test

FAQ

Copyright © 2026