Dialog
Dialog: anatomy, keyboard behavior, state, and theming in Askr.
Example
import { Button, Dialog, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from '@askrjs/themes/components';
<Dialog>
<DialogTrigger asChild><Button>Edit project</Button></DialogTrigger>
<DialogContent>
<DialogTitle>Edit project</DialogTitle>
<DialogDescription>Changes are visible immediately.</DialogDescription>
<ProjectForm />
</DialogContent>
</Dialog>Open and dismiss a modal while focus stays managed.
Published props
Generated from the TypeScript declarations shipped by the installed package. Named types in the Type column define the accepted values.
DialogCloseAsChildProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onPress | onPress?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<Element>; | — | — |
type | type?: undefined; | — | — |
DialogCloseProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onPress | onPress?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<HTMLButtonElement>; | — | — |
type | type?: "button" | "submit" | "reset" | undefined; | — | — |
DialogContentAsChildProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
forceMount | forceMount?: boolean | undefined; | — | — |
onDismiss | onDismiss?: (() => void) | undefined; | — | — |
onEscapeKeyDown | onEscapeKeyDown?: ((event: KeyboardEvent) => void) | undefined; | — | — |
onInteractOutside | onInteractOutside?: ((event: Event) => void) | undefined; | — | — |
onPointerDownOutside | onPointerDownOutside?: ((event: PointerEvent) => void) | undefined; | — | — |
ref | ref?: Ref<Element>; | — | — |
restoreFocus | restoreFocus?: HTMLElement | (() => HTMLElement | null) | null | undefined; | — | Explicit focus target used when a controlled dialog has no persistent trigger. |
role | role?: "dialog" | "alertdialog" | undefined; | — | — |
DialogContentOwnProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
forceMount | forceMount?: boolean | undefined; | — | — |
onDismiss | onDismiss?: (() => void) | undefined; | — | — |
onEscapeKeyDown | onEscapeKeyDown?: ((event: KeyboardEvent) => void) | undefined; | — | — |
onInteractOutside | onInteractOutside?: ((event: Event) => void) | undefined; | — | — |
onPointerDownOutside | onPointerDownOutside?: ((event: PointerEvent) => void) | undefined; | — | — |
restoreFocus | restoreFocus?: HTMLElement | (() => HTMLElement | null) | null | undefined; | — | Explicit focus target used when a controlled dialog has no persistent trigger. |
role | role?: "dialog" | "alertdialog" | undefined; | — | — |
DialogContentProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
forceMount | forceMount?: boolean | undefined; | — | — |
onDismiss | onDismiss?: (() => void) | undefined; | — | — |
onEscapeKeyDown | onEscapeKeyDown?: ((event: KeyboardEvent) => void) | undefined; | — | — |
onInteractOutside | onInteractOutside?: ((event: Event) => void) | undefined; | — | — |
onPointerDownOutside | onPointerDownOutside?: ((event: PointerEvent) => void) | undefined; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
restoreFocus | restoreFocus?: HTMLElement | (() => HTMLElement | null) | null | undefined; | — | Explicit focus target used when a controlled dialog has no persistent trigger. |
role | role?: "dialog" | "alertdialog" | undefined; | — | — |
DialogDescriptionAsChildProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
ref | ref?: Ref<Element>; | — | — |
DialogDescriptionProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
ref | ref?: Ref<HTMLParagraphElement>; | — | — |
DialogOverlayAsChildProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
forceMount | forceMount?: boolean | undefined; | — | — |
ref | ref?: Ref<Element>; | — | — |
DialogOverlayOwnProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
forceMount | forceMount?: boolean | undefined; | — | — |
DialogOverlayProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
forceMount | forceMount?: boolean | undefined; | — | — |
ref | ref?: Ref<HTMLDivElement>; | — | — |
DialogOwnProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | — |
defaultOpen | defaultOpen?: boolean | undefined; | — | — |
id | id?: string | undefined; | — | — |
modal | modal?: boolean | undefined; | — | — |
onOpenChange | onOpenChange?: ((open: boolean) => void) | undefined; | — | — |
open | open?: boolean | undefined; | — | — |
DialogPortalProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | — |
DialogProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
children | children?: unknown; | — | — |
defaultOpen | defaultOpen?: boolean | undefined; | — | — |
id | id?: string | undefined; | — | — |
modal | modal?: boolean | undefined; | — | — |
onOpenChange | onOpenChange?: ((open: boolean) => void) | undefined; | — | — |
open | open?: boolean | undefined; | — | — |
DialogTitleAsChildProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
ref | ref?: Ref<Element>; | — | — |
DialogTitleProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
ref | ref?: Ref<HTMLHeadingElement>; | — | — |
DialogTriggerAsChildProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild: true; | — | — |
children | children: JSXElement; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onPress | onPress?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<Element>; | — | — |
type | type?: undefined; | — | — |
DialogTriggerProps
Import from @askrjs/themes/dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | asChild?: false | undefined; | — | — |
children | children?: unknown; | — | — |
disabled | disabled?: boolean | undefined; | — | — |
onPress | onPress?: ((event: PressEvent) => void) | undefined; | — | — |
ref | ref?: Ref<HTMLButtonElement>; | — | — |
type | type?: "button" | "submit" | "reset" | undefined; | — | — |
Purpose
Trapping focus and blocking interaction with everything behind it is what Dialog does at its core, forcing the user to either act or dismiss before continuing. Reach for it when a task needs undivided attention — a form, a confirmation step, a detail view — and letting the user ignore it and keep scrolling would be the wrong call. When the content is purely informational and doesn't need to block the rest of the page, Popover is the better fit.
Install and import
Pull the behavior from @askrjs/ui/dialog and the default look from @askrjs/themes/dialog; both subpaths are real exports in their respective packages. If you're already importing from the package roots, Dialog and its parts are also re-exported from @askrjs/ui and @askrjs/themes/components directly — pick whichever import style keeps your bundle graph easiest to reason about.
Live examples
A minimal dialog is Dialog wrapping a DialogTrigger and a DialogPortal containing DialogOverlay plus DialogContent. DialogContent is where DialogTitle, DialogDescription, and any form fields or DialogClose buttons live. Toggle open/onOpenChange yourself for a controlled dialog, or leave it uncontrolled with defaultOpen and let the trigger and close buttons manage state internally.
Anatomy
The full tree is Dialog > DialogTrigger (the element that opens it) and DialogPortal > DialogOverlay (the dimmed backdrop) and DialogContent (the actual panel), with DialogTitle, DialogDescription, and DialogClose nested inside the content. Every piece is a separate component you compose yourself rather than a single component with variant props, so you can drop in your own markup between them.
State model
A dialog's open state is just a boolean, driven by open, defaultOpen, and onOpenChange on the root Dialog component. modal controls whether interaction with the rest of the page is blocked while it's open — leave it on unless you have a specific reason to allow background interaction. There's no separate loading or error state baked in; anything beyond open/closed is left to whatever you render inside DialogContent.
Keyboard and accessibility
DialogContent accepts role set to dialog or alertdialog, and wires up aria-modal, aria-labelledby from DialogTitle, and aria-describedby from DialogDescription automatically. Escape closes the dialog through onEscapeKeyDown, and clicking outside the content fires onPointerDownOutside/onInteractOutside; both are overridable if you need to intercept the close, and onDismiss fires as the common exit hook regardless of which path triggered it. Focus moves into the content on open and returns to the trigger on close.
Styling and tokens
The default theme targets the dialog by its data-slot values — dialog-overlay and dialog-content — plus data-state for open/closed transitions, so you can override appearance with plain CSS selectors instead of prop soup. The overlay backdrop uses --ak-color-backdrop with a blur and sits at --ak-z-modal-backdrop, while the content surface sits above it at --ak-z-modal and reads --ak-color-surface-raised (not the plain --ak-color-bg token — dialogs specifically use the raised-surface variant) for its background. The open and close transitions aren't symmetric: opening scales in over --ak-duration-normal, while closing scales out faster, over --ak-duration-fast — both share the same --ak-ease-standard easing, just different durations.
API
Dialog takes open, defaultOpen, onOpenChange, and modal. DialogContent adds forceMount, role, onEscapeKeyDown, onPointerDownOutside, onInteractOutside, and onDismiss on top of the usual div props, and DialogOverlay only adds forceMount. DialogTrigger and DialogClose are button-like components (they accept onPress and disabled rather than raw onClick), and every part supports asChild if you want to render your own element instead of the library's default tag.
Edge cases
Set forceMount on DialogOverlay or DialogContent when you need the nodes present in the DOM even while closed — useful for measuring layout or driving your own exit animation instead of relying on data-state. Nesting dialogs works, but each one manages its own focus trap and portal, so test the stacking order and z-index carefully if you go there. If you intercept onPointerDownOutside or onEscapeKeyDown to block dismissal (say, during a save), make sure you still give the user some other way out.
Related pages
Alert Dialog reuses Dialog behavior for confirmation flows. Sheet adds themed edge positioning while retaining the Dialog state, focus and dismissal model. Use Popover for a floating panel with its own interaction model.