V2 componentsUpdated Oct 15, 2026
Dialog
A modal window laid over the page, for a decision or a short task that needs the user before anything else happens. Content bundles the portal, backdrop, popup, and close button; Header, Body, and Footer lay out the inside at the standard spacing, so a plain Header (Title, Description), Body, Footer composition needs no classes. Trigger and Close are Buttons, so they take the same variants. The popup focuses itself on open so the title is read first; mark the field of a dialog that exists to be typed into with data-autofocus. Its height eases when the content changes, except under reduced motion. For a confirmation, see Danger confirm. An outside click or Escape dismisses every dialog.
Source: packages/ui/src/components/dialog
import { Dialog } from '@highlight/ui'Change the props; the preview and the snippet update together.
<Dialog> <Dialog.Trigger tone="danger">Open dialog</Dialog.Trigger> <Dialog.Content> <Dialog.Header> <Dialog.Title>Delete this thread?</Dialog.Title> <Dialog.Description>Messages and attachments are removed for everyone.</Dialog.Description> </Dialog.Header> <Dialog.Footer> <Dialog.Close variant="outline">Cancel</Dialog.Close> <Dialog.Close tone="danger">Confirm</Dialog.Close> </Dialog.Footer> </Dialog.Content></Dialog>import { Dialog } from '@highlight/ui' export default function DialogBasic() { return ( <Dialog> <Dialog.Trigger>View notifications</Dialog.Trigger> <Dialog.Content showClose={false}> <Dialog.Header> <Dialog.Title>Notifications</Dialog.Title> <Dialog.Description>You are all caught up.</Dialog.Description> </Dialog.Header> <Dialog.Body> <p className="text-foreground-secondary text-sm"> Every notification has been reviewed. New ones will appear here. </p> </Dialog.Body> <Dialog.Footer> <Dialog.Close variant="primary">Done</Dialog.Close> </Dialog.Footer> </Dialog.Content> </Dialog> )}Dialog.Icon goes first inside Header, a tile that says what the dialog is about.
import { CalendarCheck } from 'lucide-react'import { Dialog } from '@highlight/ui' export default function DialogWithIcon() { return ( <Dialog> <Dialog.Trigger>Connect calendar</Dialog.Trigger> <Dialog.Content showClose={false}> <Dialog.Header> <Dialog.Icon> <CalendarCheck /> </Dialog.Icon> <Dialog.Title>Connect your calendar</Dialog.Title> <Dialog.Description> Highlight joins the meetings on it and prepares notes before they start. </Dialog.Description> </Dialog.Header> <Dialog.Footer> <Dialog.Close variant="outline">Not now</Dialog.Close> <Dialog.Close variant="primary">Connect</Dialog.Close> </Dialog.Footer> </Dialog.Content> </Dialog> )}The confirm pattern now that AlertDialog is gone: Icon tone="danger", a title and description, Cancel as outline, and a primary danger Button naming the action ("Delete", not "OK"). Like any dialog, an outside click or Escape cancels it.
import { Trash2 } from 'lucide-react'import { Dialog } from '@highlight/ui' export default function DialogDangerConfirm() { return ( <Dialog> <Dialog.Trigger variant="primary" tone="danger"> Delete thread </Dialog.Trigger> <Dialog.Content showClose={false}> <Dialog.Header> <Dialog.Icon tone="danger"> <Trash2 /> </Dialog.Icon> <Dialog.Title>Delete this thread?</Dialog.Title> <Dialog.Description> Messages and attachments are removed for everyone. This cannot be undone. </Dialog.Description> </Dialog.Header> <Dialog.Footer> <Dialog.Close variant="outline">Cancel</Dialog.Close> <Dialog.Close variant="primary" tone="danger"> Delete </Dialog.Close> </Dialog.Footer> </Dialog.Content> </Dialog> )}Wrap Header, Body and Footer in a form. data-autofocus puts focus in the field on open.
import { useState, type FormEvent } from 'react'import { Button, Dialog, Field, Input } from '@highlight/ui' export default function DialogForm() { const [open, setOpen] = useState(false) function handleSubmit(event: FormEvent<HTMLFormElement>) { event.preventDefault() setOpen(false) } return ( <Dialog open={open} onOpenChange={setOpen}> <Dialog.Trigger>Rename workspace</Dialog.Trigger> <Dialog.Content showClose={false}> <form onSubmit={handleSubmit} className="flex min-h-0 flex-col"> <Dialog.Header> <Dialog.Title>Rename workspace</Dialog.Title> <Dialog.Description>Everyone in the workspace sees the new name.</Dialog.Description> </Dialog.Header> <Dialog.Body> <Field> <Field.Label>Workspace name</Field.Label> {/* data-autofocus: a dialog that exists to be typed into focuses its field, not itself. */} <Input data-autofocus name="name" defaultValue="Acme design" autoComplete="off" /> </Field> </Dialog.Body> <Dialog.Footer> <Dialog.Close variant="outline">Cancel</Dialog.Close> <Button type="submit" variant="primary"> Save </Button> </Dialog.Footer> </form> </Dialog.Content> </Dialog> )}open and onOpenChange, with the trigger elsewhere.
import { useState } from 'react'import { Button, Dialog } from '@highlight/ui' export default function DialogControlled() { const [open, setOpen] = useState(false) return ( <div className="flex items-center gap-3"> <Button variant="outline" onClick={() => setOpen(true)}> Open from outside </Button> <Dialog open={open} onOpenChange={setOpen}> <Dialog.Content> <Dialog.Header> <Dialog.Title>Controlled</Dialog.Title> <Dialog.Description>Open state lives in React.</Dialog.Description> </Dialog.Header> <Dialog.Footer> <Button variant="outline" onClick={() => setOpen(false)}> Close programmatically </Button> </Dialog.Footer> </Dialog.Content> </Dialog> </div> )}showClose={false} hides the corner button (it defaults to true); disableEscape keeps Escape and the backdrop from closing.
import { Dialog } from '@highlight/ui' export default function DialogVariants() { return ( <div className="flex flex-wrap items-center justify-center gap-3"> <Dialog> <Dialog.Trigger variant="primary" tone="danger"> Delete thread </Dialog.Trigger> <Dialog.Content showClose={false}> <Dialog.Header> <Dialog.Title>Delete this thread?</Dialog.Title> <Dialog.Description>Messages and attachments are removed for everyone.</Dialog.Description> </Dialog.Header> <Dialog.Footer> <Dialog.Close variant="outline">Cancel</Dialog.Close> <Dialog.Close variant="primary" tone="danger"> Delete </Dialog.Close> </Dialog.Footer> </Dialog.Content> </Dialog> <Dialog disableEscape> <Dialog.Trigger variant="primary">Finish setup</Dialog.Trigger> <Dialog.Content showClose={false}> <Dialog.Header> <Dialog.Title>One more step</Dialog.Title> <Dialog.Description>Escape and the backdrop are disabled; pick an action to continue.</Dialog.Description> </Dialog.Header> <Dialog.Footer> <Dialog.Close variant="outline">Later</Dialog.Close> <Dialog.Close variant="primary">Connect calendar</Dialog.Close> </Dialog.Footer> </Dialog.Content> </Dialog> </div> )}import { Dialog } from '@highlight/ui' export default function DialogCustomTrigger() { return ( <Dialog> <Dialog.Trigger nativeButton={false} render={ <span className="text-foreground-secondary hover:text-foreground cursor-pointer text-sm underline underline-offset-4"> Open from a text link </span> } /> <Dialog.Content> <Dialog.Header> <Dialog.Title>Custom trigger</Dialog.Title> <Dialog.Description> Pass render to make any element the trigger. Set nativeButton to false when it is not a button. </Dialog.Description> </Dialog.Header> </Dialog.Content> </Dialog> )}The root, then any compound sub-components. Every part accepts className and forwards the rest of its props to the element or Base UI part beneath it.
Dialog- Base UI Dialog.Root. Owns open state; renders nothing itself.
Dialog.Trigger- A Button that opens the dialog. Takes variant, tone, size, shape, or a render element.
Dialog.Content- Portal + Backdrop + Popup + close button. showClose, backdropClassName, initialFocus.
Dialog.Icon- A 36px tile over the title, first inside Header. tone is neutral (default) or danger. Takes an icon node as children; decorative, so the title carries the meaning.
Dialog.Header / Body / Footer- Layout slots. Header holds Icon, Title and Description; Body scrolls when content overflows; Footer is right-aligned with 8px between buttons.
Dialog.Title / Description- Accessible name and description.
Dialog.Close- A Button that closes the dialog.
Dialog.Portal / Backdrop / Popup- Low-level parts when Content is not enough.
Props added on top of the underlying element or Base UI part.
| Prop | Type | Default | Description |
|---|---|---|---|
disableEscape | boolean | false | Only explicit Close clicks dismiss the dialog. |