Skip to content
Devix Open Source

Guide

Getting started

Ask a question, get an answer:

npm install @devix-labs/confirm-dialog
import { confirm } from '@devix-labs/confirm-dialog';
import '@devix-labs/confirm-dialog/styles.css';

if (await confirm('Save these changes?')) save();

Or from the CDN, with no build step:

<link rel="stylesheet" href="https://devix.pk/cdn/oss/confirm-dialog@1.0.0/confirm-dialog.min.css">
<script type="module">
  import { confirm } from 'https://devix.pk/cdn/oss/confirm-dialog@1.0.0/index.js';
  document.querySelector('#save').addEventListener('click', async () => {
    if (await confirm('Save these changes?')) form.submit();
  });
</script>

The four calls

await confirm('Save these changes?');                      // true / false
await confirmDanger('Delete 42 invoices?');                // red button, Cancel focused
await alert('Your export is ready');                       // one button
await prompt('Rename the file', { defaultValue: 'report' }); // string, or null

Each takes either a string or the full options object:

await confirm({
  title: 'Publish this article?',
  description: 'It will appear on the site immediately.',
  confirmText: 'Publish',
  cancelText: 'Not yet',
  tone: 'info',
});

Destructive questions

const ok = await confirmDanger('Delete the workspace?', {
  description: 'Every project, invoice and file in it goes too.',
  requireText: 'DELETE',
  confirmText: 'Delete workspace',
});

A danger dialog is an alertdialog, opens with Cancel focused, ignores a stray click on the backdrop, and — with requireText — keeps the confirm button asleep until the words are typed exactly.

Asking for more than yes

const answer = await dialog({
  title: 'Invite a teammate',
  confirmText: 'Send invite',
  fields: [
    { name: 'email', type: 'email', label: 'Email', required: true },
    { name: 'seats', type: 'number', label: 'Seats', value: 1 },
    { name: 'admin', type: 'checkbox', label: 'Make them an admin' },
  ],
});

if (answer?.confirmed) invite(answer.values);   // { email: '…', seats: 1, admin: false }

Values come back typed: numbers as numbers, checkboxes as booleans.

Work that takes a moment

await dialog({
  title: 'Publish?',
  confirmText: 'Publish',
  onConfirm: async () => {
    await api.publish(post.id);      // the dialog stays open, showing it is busy
  },                                  // throw, and the message appears inside it
});

Why a native <dialog>

Because the browser already does the hard parts: focus is trapped inside, Esc closes it, the page behind is inert for a screen reader as well as a mouse, it renders in the top layer above everything, and focus returns to whatever opened it. Nothing here re-implements any of that.

Updated 15 Sep 2026