Live demos

Every surface Forge Dialog ships, running in this page against the same build the rest of the site uses. Each card shows the code that produced what you just saw, so nothing here is a mock-up.

Dialogs

The four entry points that replace the browser built-ins, plus the generic open() they are all built on.

alert()

One message, one button. Resolves when it closes.

await alert('This is an alert dialog.', { title: 'Heads up' });

confirm()

Resolves true or false rather than blocking the page.

const ok = await confirm('Are you sure you want to continue?', {
  title: 'Please confirm',
});

prompt()

Resolves the typed string, or null when dismissed.

const name = await prompt('What is your name?', {
  title: 'Introduce yourself',
  defaultValue: 'Ada Lovelace',
});

Validated prompt

validate returns true or an error string, and the dialog stays open until it passes.

await prompt('Enter an even number:', {
  title: 'Validated prompt',
  validate: (value) => {
    const n = Number(value);
    if (Number.isNaN(n)) return 'Please enter a number';
    if (n % 2 !== 0) return 'Number must be even';
    return true;
  },
});

open()

Build the body yourself and decide what each button resolves with.

const instance = open({
  title: 'Custom dialog',
  content: (container) => {
    const p = document.createElement('p');
    p.textContent = 'Built with the generic open() API.';
    container.append(p);
  },
  buttons: [
    { text: 'Close', role: 'primary', autoFocus: true, onClick: (i) => i.close('done') },
  ],
});
const result = await instance.whenClosed();

Stacking

Open a second dialog over the first. Focus, Escape, and scroll locking follow the top of the stack.

const first = confirm('First dialog. Open a second one on top?', { title: 'Dialog 1' });
const second = await confirm('This is stacked above the first dialog.', { title: 'Dialog 2' });
const firstResult = await first;

Animations

Set per dialog. Every animation honours prefers-reduced-motion.

No animation

Opens and closes instantly.

await alert('This alert opens and closes instantly.', { animation: 'none' });

Slide

Slides in from below instead of scaling.

await confirm('This dialog slides in from below.', { animation: 'slide' });

Beyond dialogs

The same core, presented differently. All of these live behind forgedialog/presentation.

Drawer

A side panel with the same focus handling as a modal.

drawer({
  title: 'Project settings',
  message: 'A native, focus-safe drawer.',
  side: 'right',
  buttons: [{ text: 'Done', role: 'primary', closesDialog: true }],
});

Bottom sheet

The mobile shape. Drag it down to dismiss.

bottomSheet({
  title: 'Quick actions',
  message: 'Drag down to dismiss this sheet.',
  buttons: [{ text: 'Done', role: 'primary', closesDialog: true }],
});

Toast

Announced politely, dismissed on its own. Four tones.

toast('Changes saved successfully.', { tone: 'success' });

Lightbox

An image at full size, with a caption and the same dismissal rules.

lightbox('/og-image.png', {
  alt: 'The Forge Dialog social card',
  caption: 'Rendered from site/icon.svg at build time.',
});

Loading

A blocking state you control. update() changes the message, close() ends it.

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

Command palette

Filterable commands with keyboard navigation. Type to narrow.

commandPalette([
  { id: 'theme-dark', label: 'Switch to dark theme', keywords: ['appearance'],
    run: () => setTheme('dark') },
  { id: 'theme-light', label: 'Switch to light theme', keywords: ['appearance'],
    run: () => setTheme('light') },
]);

Notification centre

Every toast raised in this page, kept in history. Raise a few toasts first, then open it.

notificationCenter();
getNotificationHistory(); // [{ id, message, tone, createdAt }]
clearNotificationHistory();

Forms and wizards

Behind forgedialog/workflows. In TypeScript the field names are inferred into the result type, so the values come back typed without a cast.

form()

A validated form from a field list. Resolves the values, or null.

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: 3 },
  ],
  { title: 'Create an account' },
);

wizard()

Multi-step, with per-step validation and shared data.

const flow = wizard({
  initialData: { name: '' },
  steps: [
    {
      id: 'profile',
      title: 'Your profile',
      render: (el, ctx) => {
        const input = document.createElement('input');
        input.className = 'fd-input';
        input.placeholder = 'Name';
        input.addEventListener('input', () => ctx.set({ name: input.value }));
        el.append(input);
      },
      validate: (ctx) => (ctx.data.name ? true : 'Please enter your name.'),
    },
    { id: 'review', title: 'Review',
      render: (el, ctx) => { el.textContent = `Ready to welcome ${ctx.data.name}.`; } },
  ],
});
const data = await flow.result;

formWizard()

The same field list as form(), spread over steps.

const values = await formWizard([
  { id: 'account', title: 'Account', fields: [
    { name: 'email', type: 'email', label: 'Email', required: true },
  ] },
  { id: 'profile', title: 'Profile', fields: [
    { name: 'name', type: 'text', label: 'Full name', required: true },
  ] },
]);

Theming

setTheme() drives the library and this page together; setThemePreset() swaps the whole look.

Theme

Light, dark, or follow the operating system.

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

Presets

Pick one, then open any demo above to see it applied.

setThemePreset('glass');

Appearance and dragging

Per-dialog colour, radius, shadow direction, hover, and drag bounds have their own interactive builder on the landing page.

open({
  title: 'Styled dialog',
  appearance: { radius: 20, shadow: { angle: 180, distance: 20, blur: 60 } },
  draggable: { bounds: 'viewport' },
});

What each demo returned

Every demo above prints its result here, along with the plugin lifecycle hooks it fired — afterOpen and afterClose from a plugin registered on this page.