API reference

Forge Dialog is a dependency-free TypeScript dialog library built on the native <dialog> element. Everything on this page is in the published package — there are no plugins to install and no peer dependencies to satisfy.

Installation

Install from npm. The package name is one word; the library is called Forge Dialog.

npm install forgedialog

Import the API and a stylesheet:

import { alert, confirm, prompt, open } from 'forgedialog';
import 'forgedialog/style.css';

Or load the browser build directly. It defines the global ForgeDialog — a JavaScript identifier, so it stays one word.

<link rel="stylesheet" href="https://unpkg.com/forgedialog/dist/index.css" />
<script src="https://unpkg.com/forgedialog/dist/index.global.js"></script>
<script>
  ForgeDialog.alert('Hello!');
</script>

Requires Chrome/Edge 88+, Firefox 78+, Safari 15.4+, or Node.js 20+ for package tooling and server-side imports.

Entry points

Every entry point is tree-shakable and has its own gzip budget enforced in CI. Import the narrowest one that covers what you need.

Import Contains
forgedialog Everything, with dragging, animations, and the full appearance applier already enabled.
forgedialog/core open(), alert, confirm, prompt, theming, labels, plugins, and the full appearance applier.
forgedialog/alert alert() alone.
forgedialog/confirm confirm() alone.
forgedialog/prompt prompt() alone.
forgedialog/presentation drawer, bottomSheet, lightbox, loading, toast, commandPalette, notificationCenter.
forgedialog/workflows form, formWizard, wizard.
forgedialog/interactions The draggable controller. Side-effectful: importing it enables dragging.
forgedialog/animations Animation presets. Side-effectful: importing it enables animations.
forgedialog/appearance The full appearance applier. Side-effectful: importing it upgrades the focused entries.
forgedialog/react useForgeDialog() for React.
forgedialog/vue useForgeDialog() for Vue.
forgedialog/svelte The dialogTrigger action.
forgedialog/web-component defineForgeDialog() and ForgeDialogElement.

Capability imports

The single-purpose entries (/alert, /confirm, /prompt) ship a lightweight appearance applier covering surface opacity, backdrop, border, and shadow presets. Add capabilities by importing them for their side effect:

import 'forgedialog/appearance';   // per-component colour, radius, hover
import 'forgedialog/interactions'; // dragging
import 'forgedialog/animations';   // animation presets
import { alert } from 'forgedialog/alert';

forgedialog and forgedialog/core already include the full appearance applier, so those imports are only needed alongside a focused entry.

Stylesheets

forgedialog/style.css is the all-in-one sheet. To ship less CSS, compose only the layers a page uses — core.css is always required.

import 'forgedialog/style/core.css';      // required
import 'forgedialog/style/forms.css';     // form fields and file dropzones
import 'forgedialog/style/workflows.css'; // wizard stepper
import 'forgedialog/style/toast.css';
import 'forgedialog/style/lightbox.css';
import 'forgedialog/style/command.css';
import 'forgedialog/style/draggable.css';

All rules live in the forgedialog cascade layer, so your own unlayered CSS wins without needing !important.

alert, confirm, prompt

Promise-returning replacements for the browser built-ins.

Signature Resolves to
alert(message, options?) Promise<void>
confirm(message, options?) Promise<boolean>false on Escape or backdrop close
prompt(message, options?) Promise<string | null>null when cancelled
await alert('Saved successfully.', { title: 'Success' });

const ok = await confirm('Delete this item?', { title: 'Please confirm' });

const name = await prompt('What is your name?', {
  defaultValue: 'Ada Lovelace',
  inputType: 'text',
  inputLabel: 'Full name',
  placeholder: 'Type a name',
  validate: (value) => (value.trim() ? true : 'Name is required'),
});

alert and confirm get role="alertdialog" automatically. prompt accepts the extra options above on top of every DialogOptions field.

open()

The low-level API. It returns a DialogInstance immediately rather than a promise, so the dialog can be updated, moved, or closed from anywhere.

import { open } from 'forgedialog';

const dialog = open({
  title: 'Custom dialog',
  content: (container) => container.append(renderForm()),
  buttons: [
    { text: 'Cancel', onClick: (i) => i.close(null) },
    { text: 'Save', role: 'primary', autoFocus: true, onClick: (i) => i.close(read()) },
  ],
});

const result = await dialog.whenClosed();          // the result, or undefined
const { result: r, reason } = await dialog.whenSettled(); // result plus why it closed

Content and untrusted HTML

content accepts a string (rendered as text), an element, or a builder function. Markup needs an explicit decision:

// Untrusted markup: pass a sanitizer.
open({ html: userContent, sanitizeHtml: (h) => DOMPurify.sanitize(h) });

// Markup you already trust.
open({ unsafeHtml: '<p>Generated by us</p>' });

Close reasons

Every close carries a reason: 'button', 'escape', 'backdrop', 'api', 'abort', or 'destroy'. Read it with whenSettled().

DialogOptions

Accepted by open() and by every helper built on it.

Option Type Description
title string Header text. Omitted titles fall back to a visually hidden label so the dialog stays named for assistive technology.
message string | HTMLElement Body text or element.
content string | HTMLElement | (container) => void Body content. Strings render as text.
html string Markup, rendered only when sanitizeHtml is also supplied.
unsafeHtml string Markup inserted as-is. Only for content you control.
sanitizeHtml (html: string) => string Sanitizer applied to html.
buttons ButtonConfig[] Footer buttons. See below.
type 'alert' | 'confirm' | 'prompt' | 'form' | 'wizard' | 'custom' Semantic kind. alert and confirm default to role="alertdialog".
role 'dialog' | 'alertdialog' Overrides the role derived from type.
size 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen' Width preset.
presentation 'modal' | 'drawer-left' | 'drawer-right' | 'bottom-sheet' | 'lightbox' How the dialog enters and where it sits.
animation 'fade' | 'scale' | 'slide' | 'spring' | 'bounce' | 'blur' | 'none' Requires the animations capability. Honours prefers-reduced-motion.
appearance DialogAppearance Per-dialog styling. See Appearance.
draggable boolean | DraggableOptions See Dragging.
closable boolean Shows the header close button. Default true.
closeOnEscape boolean Default true.
closeOnOverlayClick boolean Default true.
initialFocus string | HTMLElement | (root) => HTMLElement | null What receives focus on open. Defaults to the first tabbable element.
restoreFocus boolean Returns focus to the trigger on close. Default true.
portalTarget HTMLElement Where the dialog mounts. Defaults to document.body.
className string Extra classes on the dialog element.
labels Partial<DialogLabels> Per-dialog label overrides.
signal AbortSignal Aborting closes the dialog with reason 'abort'.
data unknown Arbitrary payload, readable from hooks.
onOpen (instance) => void | Promise<void> After the dialog opens.
onBeforeClose (instance, result) => boolean | void | Promise<…> Return false to keep it open.
onClose (instance, result) => void | Promise<void> After the dialog closes.
onError (error, instance) => void Observes lifecycle failures, which are cleaned up automatically.

ButtonConfig

Field Type Description
text string Label. Required.
role 'primary' | 'secondary' | 'danger' Visual weight.
onClick (instance) => void | Promise<void> Click handler.
closesDialog boolean Closes the dialog after onClick.
result TResult Value the dialog resolves with when this button closes it.
autoFocus boolean Receives focus on open.
disabled boolean Renders the button disabled.
id string Element id, useful for tests.

DialogInstance

Member Returns Description
open() Promise<void> Opens a dialog that is not open yet.
close(result?, reason?) Promise<void> Closes with a result.
cancel(reason?) Promise<void> Closes without a result.
destroy() Promise<void> Closes and removes the element and its listeners.
update(partialOptions) void Restyles or re-renders a dialog in place, while it is open.
whenClosed() Promise<TResult | undefined> Resolves with the result.
whenSettled() Promise<{ result, reason }> Resolves with the result and the close reason.
isOpen() boolean Whether it is currently open.
getState() DialogState 'idle' | 'opening' | 'open' | 'closing' | 'closed' | 'destroyed'.
getPosition() { x, y } Current drag offset.
setPosition({ x, y }) { x, y } Moves the dialog, clamped to its bounds.
resetPosition() void Returns it to centre.
element HTMLElement The dialog element.
id string Unique id.

Appearance

Appearance overrides are scoped to one dialog and can be changed at runtime with update(). Opacity values are clamped to 0..1; plain numbers for widths, radii, and blur are read as pixels.

Field Type Applies to
opacity number The whole dialog surface.
surfaceColor string Dialog background.
overlayOpacity number The backdrop behind it.
backdropBlur number | string Backdrop blur radius.
titleColor string Header text.
titleBackground string Header fill. Transparent by default and clipped by the dialog's corners.
titleOpacity number Header.
contentColor string Body text.
contentOpacity number Body.
borderColor string Border.
borderOpacity number Border.
borderWidth number | string Border.
borderStyle 'none' | 'solid' | 'dashed' | 'dotted' | 'double' Border.
radius number | string | DialogCornerRadius Corners. See Corner radius.
shadow ShadowPreset | string | DialogShadowConfig See Shadows.
hover DialogHoverAppearance See Hover.
open({
  title: 'Styled',
  appearance: {
    opacity: 0.96,
    surfaceColor: '#12141a',
    overlayOpacity: 0.55,
    backdropBlur: 12,
    titleColor: '#ffd166',
    titleBackground: '#1b1740',
    contentColor: '#c8ccd4',
    borderColor: '#7c5cff',
    borderWidth: 2,
    radius: 20,
    shadow: { angle: 135, distance: 24, blur: 60, opacity: 0.45 },
  },
});

The full applier ships with forgedialog and forgedialog/core. On a focused entry, add import 'forgedialog/appearance' — without it the richer fields are ignored rather than throwing.

Shadows

shadow takes three shapes.

Form Example
A preset shadow: 'xl' — one of none, sm, md, lg, xl
Any CSS box-shadow shadow: '0 10px 40px rgb(0 0 0 / 30%)'
Composed from parts shadow: { angle: 90, distance: 18, blur: 48, opacity: 0.3 }
Part Type Meaning
angle number Direction the shadow falls, in degrees: 0 up, 90 right, 180 down (the default), 270 left.
distance number | string How far it is thrown.
blur number | string Softness.
spread number | string Growth before blurring.
color string Shadow colour.
opacity number Strength, 0..1.
inset boolean Casts the shadow inwards.

Corner radius

radius takes one value for every corner, any CSS border-radius string, or a per-corner object. Corners left out of that object keep the theme radius rather than collapsing to zero.

appearance: { radius: 20 }
appearance: { radius: '20px 20px 4px 4px' }
appearance: { radius: { topLeft: 24, topRight: 24, bottomRight: 4, bottomLeft: 4 } }

Drawers stay square and bottom sheets round only their top corners unless radius says otherwise.

Hover

hover restyles a dialog only while the pointer is over it, and only for dialogs that ask for it — every other dialog stays completely static. It reuses the colour, radius, and shadow fields and adds three of its own.

Field Type Description
lift number | string How far the dialog rises.
scale number Scale factor, e.g. 1.02.
duration number | string Transition length.

Hover transforms honour prefers-reduced-motion and are suspended while a dialog is being dragged, so they cannot push it outside its bounds.

Dragging

draggable: true is enough to move a dialog by its header. The object form adds constraints, keyboard support, and persistence. Dragging needs the interactions capability, which forgedialog enables for you.

Option Type Description
handle 'header' | string | HTMLElement What starts a drag. Defaults to the header.
axis 'both' | 'x' | 'y' Locks movement to one axis.
bounds 'viewport' | HTMLElement | DOMRect Area the dialog is clamped to.
initialPosition { x, y } Starting offset.
keyboard boolean Arrow-key movement when the handle has focus.
keyboardStep number Pixels per key press.
persistKey string Remembers the position across sessions under this key.
onDragStart / onDrag / onDragEnd (event) => void Each receives { position, originalEvent }.

Bottom sheets keep their own swipe-to-dismiss gesture and ignore general dragging.

Drawers, sheets, lightboxes, loading

import { drawer, bottomSheet, lightbox, loading } from 'forgedialog';

drawer({ title: 'Settings', side: 'right', content: renderSettings });

bottomSheet({ title: 'Share', content: renderShare });

lightbox('/photo.jpg', { alt: 'A photo', caption: 'Taken in 2026' });

const task = loading('Uploading…');
task.update('Almost there…');
await task.close();

These are open() with a presentation preset, so every DialogOptions field still applies.

Forms

form() builds a validated dialog from a field list and resolves with the values, or null if cancelled. Field names are inferred into the result type, so values.email is typed without a cast.

import { form } from 'forgedialog';

const values = await form(
  [
    { name: 'email', type: 'email', label: 'Email', required: true },
    { name: 'plan', type: 'select', label: 'Plan', options: [
      { value: 'free', label: 'Free' },
      { value: 'pro', label: 'Pro' },
    ] },
    { name: 'notes', type: 'textarea', label: 'Notes', rows: 4 },
  ],
  { title: 'Sign up', submitText: 'Create account' },
);

Field types: text, password, email, number, date, textarea, select, checkbox, radio, file. Each field takes label, defaultValue, placeholder, helpText, required, and a validate function; numbers add min, max, step, and files add accept, multiple, maxFiles, and maxSizeBytes.

formWizard(steps, options?) spreads the same fields over several steps, with nextText, backText, finishText, and an onStepChange callback.

Wizards

wizard() is the general form: each step renders whatever it likes, can validate, and can choose the next step from the data collected so far.

import { wizard } from 'forgedialog';

const flow = wizard({
  initialData: { email: '', plan: 'free' },
  persistKey: 'signup',      // resumes where the user left off
  confirmUnsaved: true,      // asks before discarding progress
  steps: [
    { id: 'account', title: 'Account', render: renderAccount, validate: validateAccount },
    { id: 'plan', title: 'Plan', render: renderPlan,
      next: (data) => (data.plan === 'pro' ? 'billing' : 'review') },
    { id: 'billing', title: 'Billing', render: renderBilling },
    { id: 'review', title: 'Review', render: renderReview },
  ],
});

const data = await flow.result; // the data, or null if cancelled

The controller also exposes next(), back(), goTo(stepId), getData(), and the underlying instance.

Toasts and notifications

import { toast, notificationCenter, getNotificationHistory } from 'forgedialog';

toast('Saved', { tone: 'success' });

// A duration of 0 or Infinity keeps it up until dismiss() is called.
const upload = toast('Uploading…', { duration: 0 });
upload.dismiss();

toast('Message sent', { action: { text: 'Undo', onClick: undoSend } });

notificationCenter();          // a dialog listing past toasts
getNotificationHistory();      // the same list as data

Tones are info, success, warning, and danger. clearNotificationHistory() empties the log.

Command palette

import { commandPalette } from 'forgedialog';

commandPalette([
  { id: 'new', label: 'New document', shortcut: '⌘N', run: createDocument },
  { id: 'search', label: 'Search', keywords: ['find', 'filter'], run: openSearch },
]);

Typing filters on the label and on keywords; the palette resolves with the id of the command that ran.

Theming

import { setTheme, getTheme, setThemePreset } from 'forgedialog';

setTheme('dark');    // 'light' | 'dark' | 'system'
getTheme();

setThemePreset('glass'); // 'default' | 'minimal' | 'glass' | 'material'

setTheme() writes data-fd-theme on <html>. Your own stylesheet can read the same attribute so the page and the dialogs switch together — that is exactly what this site does.

CSS custom properties

Everything visual is a variable on :root, so a theme can be written without touching JavaScript.

Variable Default
--fd-color-surface #ffffff
--fd-color-text #1a1d23
--fd-color-text-muted #5b6270
--fd-color-border #e2e4e9
--fd-color-overlay rgba(15, 17, 21, 0.5)
--fd-color-primary / --fd-color-primary-text #315fce / #ffffff
--fd-color-secondary / --fd-color-secondary-text #eceef2 / #1a1d23
--fd-color-danger / --fd-color-danger-text #e5484d / #ffffff
--fd-radius 10px
--fd-shadow 0 20px 60px rgba(15, 17, 21, 0.25)
--fd-spacing-sm / md / lg 8px / 16px / 24px
--fd-font-family system UI stack
--fd-duration-fast / --fd-duration-normal 120ms / 200ms
--fd-easing cubic-bezier(0.16, 1, 0.3, 1)
--fd-z-index-base 1000

The per-dialog variables the appearance API writes (--fd-dialog-radius, --fd-dialog-shadow, --fd-dialog-title-background, and the rest) can also be set by hand if you would rather style in CSS than in JavaScript.

Labels and i18n

import { setLabels, getLabels } from 'forgedialog';

setLabels({
  ok: 'Đồng ý',
  cancel: 'Huỷ',
  close: 'Đóng',
  submit: 'Gửi',
  fieldRequired: 'Bắt buộc',
  promptPlaceholder: 'Nhập nội dung',
  notifications: 'Thông báo',
});

Labels set this way apply everywhere; a single dialog can override them with its own labels option.

Plugins and hooks

Five lifecycle hooks fire for every dialog: beforeOpen, afterOpen, beforeClose, afterClose, and beforeDestroy.

import { on, off, registerPlugin } from 'forgedialog';

// A single hook.
on('afterOpen', ({ instance, options }) => track('dialog', options.title));

// Or a plugin bundling several.
registerPlugin({
  name: 'analytics',
  install: (api) => api.on('afterClose', ({ reason }) => track('close', reason)),
  hooks: {
    beforeClose: ({ preventClose, result }) => {
      if (result === undefined) preventClose?.();
    },
  },
});

A hook receives { instance, options, result, reason, preventClose }. Calling preventClose() in beforeClose keeps the dialog open.

Framework adapters

Each adapter is a thin wrapper over open() that ties a dialog's lifetime to the component's, so nothing is left mounted after unmount.

// React and Vue share the same hook shape.
import { useForgeDialog } from 'forgedialog/react';
const { open } = useForgeDialog();
await open({ title: 'Hi' }).whenClosed();

// Svelte: an action on the triggering element.
import { dialogTrigger } from 'forgedialog/svelte';
// <button use:dialogTrigger={{ title: 'Hi' }}>Open</button>

// Web component.
import { defineForgeDialog } from 'forgedialog/web-component';
defineForgeDialog(); // <forge-dialog open title="Hi" message="…" size="md">

TypeScript, SSR, and CSP

  • Types ship for both ESM and CJS. open<T>() threads its result type through buttons, whenClosed(), and the hooks.
  • Importing the package on a server does not touch the DOM, so it is safe in SSR builds; the dialog is only created when you call the API in a browser.
  • The bundle contains no eval and no new Function, which is enforced in CI, so it runs under a strict Content Security Policy. This site is served under one.
  • Every entry point has an enforced gzip budget, checked on each pull request.