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