Skip to main content

Overview

The dialog widget is a simplified API for creating confirmation dialogs with declarative action buttons. It’s syntactic sugar over the modal widget, designed for common confirmation, alert, and prompt flows.

Basic Usage

import { ui } from "@rezi-ui/core";
import { defineWidget } from "@rezi-ui/core";

const DialogExample = defineWidget((ctx) => {
  const [showDialog, setShowDialog] = ctx.useState(false);

  return ui.layers([
    ui.column({ gap: 1, p: 1 }, [
      ui.button({
        id: "open-dialog",
        label: "Open Dialog",
        onPress: () => setShowDialog(true),
      }),
    ]),
    showDialog &&
      ui.dialog({
        id: "confirm-dialog",
        title: "Confirm Action",
        message: "Are you sure you want to proceed?",
        actions: [
          {
            label: "Cancel",
            onPress: () => setShowDialog(false),
          },
          {
            label: "Confirm",
            intent: "primary",
            onPress: () => {
              handleConfirm();
              setShowDialog(false);
            },
          },
        ],
        onClose: () => setShowDialog(false),
      }),
  ]);
});

Props

id
string
required
Unique identifier for focus routing.
message
string | VNode
required
Dialog message content. Can be plain text or any VNode.
actions
readonly DialogAction[]
required
Array of action button descriptors.
title
string
Optional title rendered in dialog header.
onClose
() => void
Callback when dialog should close.
Dialog accepts all modal props except content and actions:
width
number | 'auto'
Dialog width in cells.
height
number
Dialog height in cells.
backdrop
ModalBackdrop
default:"'dim'"
Backdrop style: "none", "dim", "opaque", or custom config.
closeOnBackdrop
boolean
default:"true"
Close when backdrop is clicked.
closeOnEscape
boolean
default:"true"
Close when ESC key is pressed.
initialFocus
string
ID of element to focus when dialog opens.
returnFocusTo
string
ID of element to return focus to when dialog closes.

DialogAction Interface

type DialogAction = {
  id?: string;
  label: string;
  intent?: "primary" | "danger";
  onPress: () => void;
  disabled?: boolean;
  focusable?: boolean;
};
label
string
required
Button label text.
onPress
() => void
required
Callback when button is pressed.
intent
DialogActionIntent
Button intent:
  • "primary" - Primary call-to-action
  • "danger" - Destructive action
id
string
Optional button ID. Auto-generated if omitted.
disabled
boolean
Disable the action button.
focusable
boolean
Control focus order participation.

Keyboard Controls

KeyAction
ESCClose dialog
TabNavigate between action buttons
EnterActivate focused button

Confirmation Dialog

import { ui } from "@rezi-ui/core";

ui.dialog({
  id: "confirm",
  title: "Confirm Action",
  message: "Are you sure you want to continue?",
  actions: [
    {
      label: "Cancel",
      onPress: () => closeDialog(),
    },
    {
      label: "Continue",
      intent: "primary",
      onPress: () => {
        performAction();
        closeDialog();
      },
    },
  ],
  onClose: closeDialog,
});

Destructive Confirmation

import { ui } from "@rezi-ui/core";

ui.dialog({
  id: "delete-confirm",
  title: "Delete Item",
  message: "This action cannot be undone. Are you sure?",
  actions: [
    {
      label: "Cancel",
      onPress: () => closeDialog(),
    },
    {
      label: "Delete",
      intent: "danger",
      onPress: () => {
        deleteItem();
        closeDialog();
      },
    },
  ],
  backdrop: "opaque",
  closeOnBackdrop: false, // Force explicit choice
  closeOnEscape: false,
  onClose: closeDialog,
});

Alert Dialog

import { ui } from "@rezi-ui/core";

ui.dialog({
  id: "alert",
  title: "Operation Complete",
  message: "Your changes have been saved successfully.",
  actions: [
    {
      label: "OK",
      intent: "primary",
      onPress: () => closeDialog(),
    },
  ],
  onClose: closeDialog,
});

Multi-Action Dialog

import { ui } from "@rezi-ui/core";

ui.dialog({
  id: "unsaved-changes",
  title: "Unsaved Changes",
  message: "You have unsaved changes. What would you like to do?",
  actions: [
    {
      label: "Discard",
      intent: "danger",
      onPress: () => {
        discardChanges();
        closeDialog();
      },
    },
    {
      label: "Cancel",
      onPress: () => closeDialog(),
    },
    {
      label: "Save",
      intent: "primary",
      onPress: () => {
        saveChanges();
        closeDialog();
      },
    },
  ],
  closeOnBackdrop: false,
  onClose: closeDialog,
});

Rich Content Dialog

Use VNode for complex message content:
import { ui } from "@rezi-ui/core";

ui.dialog({
  id: "rich-dialog",
  title: "Delete Multiple Items",
  message: ui.column({ gap: 1 }, [
    ui.text("The following items will be deleted:"),
    ui.column({ gap: 0, pl: 2 }, [
      ui.text("• Document.pdf"),
      ui.text("• Image.png"),
      ui.text("• Video.mp4"),
    ]),
    ui.text("This action cannot be undone.", { style: { bold: true } }),
  ]),
  actions: [
    {
      label: "Cancel",
      onPress: closeDialog,
    },
    {
      label: "Delete All",
      intent: "danger",
      onPress: () => {
        deleteItems();
        closeDialog();
      },
    },
  ],
  onClose: closeDialog,
});

Disabled Actions

import { defineWidget } from "@rezi-ui/core";

const ValidatedDialog = defineWidget((ctx) => {
  const [agreed, setAgreed] = ctx.useState(false);

  return ui.dialog({
    id: "terms-dialog",
    title: "Terms and Conditions",
    message: ui.column({ gap: 1 }, [
      ui.text("Please read and accept the terms and conditions."),
      ui.checkbox({
        id: "terms-checkbox",
        checked: agreed,
        label: "I agree to the terms and conditions",
        onChange: setAgreed,
      }),
    ]),
    actions: [
      {
        label: "Cancel",
        onPress: closeDialog,
      },
      {
        label: "Accept",
        intent: "primary",
        disabled: !agreed,
        onPress: () => {
          acceptTerms();
          closeDialog();
        },
      },
    ],
    closeOnBackdrop: false,
    closeOnEscape: false,
    onClose: closeDialog,
  });
});

Error Dialog

import { ui } from "@rezi-ui/core";

ui.dialog({
  id: "error-dialog",
  title: "Error",
  message: ui.column({ gap: 1 }, [
    ui.text("An error occurred:", { style: { bold: true } }),
    ui.text(error.message, { style: { fg: { r: 255, g: 100, b: 100 } } }),
    error.stack && ui.text(error.stack, { variant: "code", dim: true }),
  ]),
  actions: [
    {
      label: "Copy Error",
      onPress: () => {
        copyToClipboard(error.message);
      },
    },
    {
      label: "Close",
      intent: "primary",
      onPress: closeDialog,
    },
  ],
  width: 60,
  onClose: closeDialog,
});

Loading Dialog

import { ui } from "@rezi-ui/core";

ui.dialog({
  id: "loading-dialog",
  title: "Processing",
  message: ui.column({ gap: 1, items: "center" }, [
    ui.spinner({ variant: "dots" }),
    ui.text("Please wait..."),
  ]),
  actions: [], // No actions during loading
  closeOnBackdrop: false,
  closeOnEscape: false,
});

Dialog vs Modal

Use Dialog when:
  • Simple confirmation or alert
  • 1-3 action buttons
  • Plain text or simple message content
Use Modal when:
  • Complex forms or multi-step flows
  • Custom layout control needed
  • Dynamic action buttons (not declarative array)
// Dialog: Simple and declarative
ui.dialog({
  id: "simple",
  message: "Confirm?",
  actions: [
    { label: "No", onPress: close },
    { label: "Yes", intent: "primary", onPress: confirm },
  ],
});

// Modal: Complex and flexible
ui.modal({
  id: "complex",
  title: "Edit Profile",
  content: ui.form([...formFields]),
  actions: [cancelButton, saveButton],
});

Best Practices

  1. Keep messages concise - One or two sentences max
  2. Use intent correctly - "primary" for safe actions, "danger" for destructive
  3. Provide escape route - Always include Cancel or Close option
  4. Focus primary action - Use initialFocus to direct keyboard users
  5. Disable backdrop close - For critical decisions, set closeOnBackdrop: false
  6. Rich content sparingly - Use VNodes only when necessary

Accessibility

  • Focus automatically trapped within dialog
  • Tab navigates between action buttons
  • ESC closes dialog (unless disabled)
  • Action buttons are keyboard accessible
  • Screen readers announce dialog title and message
  • Modal - For complex dialogs with custom layout
  • Callout - For inline alert messages
  • Toast - For non-blocking notifications

Location in Source

  • Types: packages/core/src/widgets/types.ts:1090-1109
  • Factory: packages/core/src/widgets/ui.ts:1208

Build docs developers (and LLMs) love