Askr documentation
UI & Components

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>
Try 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/ui/dialog.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
disableddisabled?: boolean | undefined;
onPressonPress?: ((event: PressEvent) => void) | undefined;
refref?: Ref<Element>;
typetype?: undefined;

DialogCloseProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
disableddisabled?: boolean | undefined;
onPressonPress?: ((event: PressEvent) => void) | undefined;
refref?: Ref<HTMLButtonElement>;
typetype?: "button" | "submit" | "reset" | undefined;

DialogContentAsChildProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
forceMountforceMount?: boolean | undefined;
onDismissonDismiss?: (() => void) | undefined;
onEscapeKeyDownonEscapeKeyDown?: ((event: KeyboardEvent) => void) | undefined;
onInteractOutsideonInteractOutside?: ((event: Event) => void) | undefined;
onPointerDownOutsideonPointerDownOutside?: ((event: PointerEvent) => void) | undefined;
refref?: Ref<Element>;
rolerole?: "alertdialog" | "dialog" | undefined;

DialogContentOwnProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
forceMountforceMount?: boolean | undefined;
onDismissonDismiss?: (() => void) | undefined;
onEscapeKeyDownonEscapeKeyDown?: ((event: KeyboardEvent) => void) | undefined;
onInteractOutsideonInteractOutside?: ((event: Event) => void) | undefined;
onPointerDownOutsideonPointerDownOutside?: ((event: PointerEvent) => void) | undefined;
rolerole?: "alertdialog" | "dialog" | undefined;

DialogContentProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
forceMountforceMount?: boolean | undefined;
onDismissonDismiss?: (() => void) | undefined;
onEscapeKeyDownonEscapeKeyDown?: ((event: KeyboardEvent) => void) | undefined;
onInteractOutsideonInteractOutside?: ((event: Event) => void) | undefined;
onPointerDownOutsideonPointerDownOutside?: ((event: PointerEvent) => void) | undefined;
refref?: Ref<HTMLDivElement>;
rolerole?: "alertdialog" | "dialog" | undefined;

DialogDescriptionAsChildProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
refref?: Ref<Element>;

DialogDescriptionProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
refref?: Ref<HTMLParagraphElement>;

DialogOverlayAsChildProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
forceMountforceMount?: boolean | undefined;
refref?: Ref<Element>;

DialogOverlayOwnProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
forceMountforceMount?: boolean | undefined;

DialogOverlayProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
forceMountforceMount?: boolean | undefined;
refref?: Ref<HTMLDivElement>;

DialogOwnProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
childrenchildren?: unknown;
defaultOpendefaultOpen?: boolean | undefined;
idid?: string | undefined;
modalmodal?: boolean | undefined;
onOpenChangeonOpenChange?: ((open: boolean) => void) | undefined;
openopen?: boolean | undefined;

DialogPortalProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
childrenchildren?: unknown;

DialogProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
childrenchildren?: unknown;
defaultOpendefaultOpen?: boolean | undefined;
idid?: string | undefined;
modalmodal?: boolean | undefined;
onOpenChangeonOpenChange?: ((open: boolean) => void) | undefined;
openopen?: boolean | undefined;

DialogTitleAsChildProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
refref?: Ref<Element>;

DialogTitleProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
refref?: Ref<HTMLHeadingElement>;

DialogTriggerAsChildProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
asChildasChild: true;
childrenchildren: JSXElement;
disableddisabled?: boolean | undefined;
onPressonPress?: ((event: PressEvent) => void) | undefined;
refref?: Ref<Element>;
typetype?: undefined;

DialogTriggerProps

Import from @askrjs/ui/dialog.

PropTypeDefaultDescription
asChildasChild?: false | undefined;
childrenchildren?: unknown;
disableddisabled?: boolean | undefined;
onPressonPress?: ((event: PressEvent) => void) | undefined;
refref?: Ref<HTMLButtonElement>;
typetype?: "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.

Alert Dialog reuses this exact component tree under the hood for the case where you specifically need a confirm/cancel action pair. Drawer and Sheet are also Dialog under a different name in the themed layer, just with slide-in positioning instead of a centered panel. If what you actually want is a non-modal floating panel, see Popover.