Dialog
Dialog
<DialogTrigger> <Button>Edit profile</Button> <Dialog title="Edit profile" description="Update your visible information."> {({ close }) => ( <Stack gap={4}> <TextInput label="Name" /> <Stack direction="row" justify="end" gap={2}> <Button variant="ghost" tone="neutral" onPress={close}>Cancel</Button> <Button onPress={close}>Save</Button> </Stack> </Stack> )} </Dialog></DialogTrigger>Install: @helixui/core
import { Dialog, DialogTrigger } from '@helixui/core'status: stable · since: 0.1.0
Tags: modal, blocking, confirm, focus-trap, dismissable
Anatomy
┌─ overlay (full screen, dimmed) ────────────────────────┐│ ││ ┌── dialog panel ─────────────────────────┐ ││ │ Title (h2 slot) │ ││ │ Description │ ││ │ │ ││ │ <children — usually a Stack form> │ ││ │ │ ││ │ [ Cancel ] [ Confirm ] ← actions row │ ││ └──────────────────────────────────────────┘ ││ │└────────────────────────────────────────────────────────┘ ↑ trapped focus, dismiss on overlay-click + Escape (when isDismissable)Layout
- display —
portal - width —
fixed:380/540/720px (sm/md/lg) - height —
content - intrinsicSize —
centered, max-w by size, max-h ~80vh, scrolls inside - stackable —
false - fullBleed —
false
Visual
A centered surface (radius.xl) on a dimmed overlay. The panel uses surface.default with a strong shadow (shadow.lg) and ample padding (space.6). Title is large semibold (font.size.xl + font.weight.semibold), description in muted secondary text below. Mounts with a subtle scale-and-fade (respects prefers-reduced-motion). Body scrolls internally when it overflows; header and footer remain pinned only if you compose them with Stack.
Props
| Name | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | Dialog title — rendered as <h2 slot="title"> for screen readers. |
description | ReactNode | — | Lead paragraph below the title. |
size | 'sm' | 'md' | 'lg' | md | Max width — 380 / 540 / 720 px. |
isDismissable | boolean | true | Allow close via overlay click and Escape. |
isOpen | boolean | — | Controlled open state. |
defaultOpen | boolean | false | Uncontrolled initial open. |
onOpenChange | (open: boolean) => void | — | Open change handler. |
Slots
- DialogTrigger child — the button that opens the dialog
- Dialog children — content; render-prop receives
{ close }to close imperatively
Tokens used
color.bg.surface.default, color.text.primary, color.text.secondary, radius.xl, space.4, space.6, space.12, font.family.sans, font.size.sm, font.size.xl, font.weight.semibold, font.lineHeight.tight, shadow.lg
Accessibility
Role: dialog
Keyboard
| Key | Action |
|---|---|
Tab | Cycles through focusable content. Focus is trapped. |
Escape | Closes the dialog when isDismissable. |
Notes
- Always provide
title(or setaria-labelon the Dialog) so screen readers announce the dialog’s purpose. - Animations respect
prefers-reduced-motion.
Composes with
| Component | Relation | Note |
|---|---|---|
DialogTrigger | parent | Required wrapper that provides the open trigger element. |
Button | trigger | Most common trigger inside DialogTrigger. |
Stack | child | Recommended layout for the dialog body and its actions row. |
Form | child | Forms inside dialogs submit and then close via the render-prop close(). |
Sheet | alternative | On small screens prefer Sheet (side=‘bottom’) for the same task. |
Popover | alternative | Use Popover for non-blocking, anchored interactions. |
Prompt examples
These are the AI prompt → JSX mappings used by the helixui prompt DSL and integrations like Cursor / Claude Code.
destructive confirmation
“delete account confirmation modal”
<DialogTrigger> <Button tone="danger">Delete account</Button> <Dialog title="Delete account?" description="This cannot be undone."> {({ close }) => ( <Stack direction="row" justify="end" gap={2}> <Button variant="ghost" onPress={close}>Cancel</Button> <Button tone="danger" onPress={close}>Delete</Button> </Stack> )} </Dialog></DialogTrigger>small form in a modal
“modal to rename a project”
<DialogTrigger> <Button variant="soft">Rename</Button> <Dialog title="Rename project" size="sm"> {({ close }) => ( <Stack gap={3}> <TextInput label="Name" /> <Stack direction="row" justify="end" gap={2}> <Button variant="ghost" onPress={close}>Cancel</Button> <Button onPress={close}>Save</Button> </Stack> </Stack> )} </Dialog></DialogTrigger>non-dismissable required choice
“accept terms blocking modal that cannot be dismissed”
<Dialog isOpen title="Accept the terms" isDismissable={false}> {({ close }) => ( <Stack gap={4}> <Text>You must accept the terms to continue.</Text> <Button onPress={close}>I agree</Button> </Stack> )}</Dialog>controlled open/close
“dialog whose open state lives in my component”
const [open, setOpen] = useState(false);<Dialog isOpen={open} onOpenChange={setOpen} title="Edit"> ...</Dialog>