@twab/confirmation-box
vue-confirmation-box
A lightweight, promise-based confirmation dialog for Vue 2 — no UI framework required, just Vue and Font Awesome for the default icon.
Instead of wiring up a visible boolean, a dialog component, and callback props for every confirmation prompt in your app, you install a plugin once and then just:
const confirmed = await this.$confirmationBox('Are you sure?')
if (confirmed) {
// ...
}
Requirements
- Vue
^2.7.0(peer dependency — this package ships Vue 2.7's built-in compiler, no separatevue-template-compilerneeded) @fortawesome/fontawesome-free— installed automatically as a dependency of this package (so it's in yournode_modulesalready), but its CSS is not bundled into this library's own output to keep it lean. Import it once yourself:This is what the default icon (and anyimport '@fortawesome/fontawesome-free/css/fontawesome.css' import '@fortawesome/fontawesome-free/css/solid.css'fas fa-*icon you pass) needs to render. If you use a different icon font instead (mdi, a different Font Awesome style, ...), see theiconoption below — any icon-font class name works, Font Awesome is just the bundled default.
That's it — no UI framework (Vuetify or otherwise) is required. The dialog is self-contained plain HTML/CSS.
Installation
npm install @twab/confirmation-box
Setup
The package ships prebuilt (plain JS, no .vue compilation needed on your end) and its CSS is injected automatically at runtime — there's no separate stylesheet to import for the dialog itself:
import Vue from 'vue'
import '@fortawesome/fontawesome-free/css/fontawesome.css'
import '@fortawesome/fontawesome-free/css/solid.css'
import ConfirmationBox from '@twab/confirmation-box'
Vue.use(ConfirmationBox, {
// optional global defaults — see "Global install options" below
})
new Vue({
render: (h) => h(App),
}).$mount('#app')
Calling Vue.use(ConfirmationBox, ...) more than once is a no-op — only the first call's options are used.
The dialog is mounted into document.querySelector('[data-app=true]') if present, falling back to document.body. This exists for apps that also happen to use Vuetify elsewhere (whose <v-app> sets data-app="true") so the dialog nests correctly with it — it's entirely optional and has no effect if you don't use Vuetify.
Upgrading from a version that depended on Vuetify? This is a breaking change:
Vue.use(ConfirmationBox, vuetify, options)(3 args) is nowVue.use(ConfirmationBox, options)(2 args) — drop the Vuetify instance argument. Vuetify is no longer required at all. If your own CSS targeted.confirmation-box .v-iconor.confirmation-button .v-btn, update those selectors to.confirmation-box .confirmation-box__iconand.confirmation-box__button(see the CSS classes below).
Basic usage
Once installed, every component instance has this.$confirmationBox(...), which returns a Promise.
async deleteItem() {
const confirmed = await this.$confirmationBox('Delete this item?')
if (confirmed) {
await api.delete(this.item.id)
}
}
Passing a string is shorthand for { message: 'your string' }.
Full example
async deleteItem() {
const result = await this.$confirmationBox({
title: 'Delete item',
message: 'This action <strong>cannot</strong> be undone.',
icon: 'mdi-delete',
color: 'error',
width: 500,
clickOutside: false,
buttons: [
{ text: 'Yes', result: true, color: 'success', shortcut: 'Enter' },
{ text: 'No', result: false, color: 'error', shortcut: 'keyN' },
],
})
if (result) {
await api.delete(this.item.id)
}
}
The promise resolves with whatever value is attached to the button that was activated — result can be a boolean, string, number, or object, so you're not limited to yes/no.
API — per-call options
Passed as the object argument to this.$confirmationBox({ ... }):
| Option | Type | Default | Description |
|---|---|---|---|
message |
String |
'' |
Body text shown under the title. Rendered with v-html — do not pass unsanitized user input, it's an XSS vector. |
title |
String |
'Are you sure?' |
Dialog heading. |
icon |
String |
'fas fa-circle-question' |
Icon-font class name. A single token like 'mdi-delete' or 'fa-trash' automatically gets its base class (mdi/fa) prefixed; pass a full compound string (e.g. 'fas fa-trash') if your icon font needs something else. The default requires the Font Awesome CSS import from Requirements above; a custom value works with whatever icon font you load instead. |
color |
String |
'primary' |
Color applied to the icon. Accepts the classic Vuetify semantic names (primary, secondary, accent, error, info, success, warning — mapped to their original Material colors, see below) or any CSS color (hex, rgb(), named color, ...). |
width |
String | Number |
350 |
Dialog width. A number is treated as pixels; a string is used as-is (e.g. '80%'). |
clickOutside |
Boolean | String | Number | Object |
false |
See "Closing by clicking outside" below. |
fields |
Array<Field> |
[] |
Optional simple form controls (text/number/checkbox) shown above the buttons — see "Fields" below. |
buttons |
Array<Button> |
[{ text: 'Ok', result: true, color: 'primary' }] |
Buttons rendered left-to-right at the bottom of the dialog. |
Named colors
color (top-level, for the icon) and each button's color accept these names, mapped to the same hex values as Vuetify 2's default theme (for visual continuity if you're migrating off it):
| Name | Hex |
|---|---|
primary |
#1976D2 |
secondary |
#424242 |
accent |
#82B1FF |
error |
#FF5252 |
info |
#2196F3 |
success |
#4CAF50 |
warning |
#FB8C00 |
Anything else (e.g. '#123456', 'rebeccapurple') is passed straight through as a CSS color. This is a fixed lookup table, not a theme — there's no way to customize these names to different colors; use a raw CSS color value if you need something else.
Fields
For a quick input or two alongside the message (a number, a couple of checkboxes), add fields — each renders above the buttons, bound to its own value for the lifetime of the dialog:
fields: [
{
key: 'expandSeconds',
type: 'number',
label: 'Expand (seconds)',
placeholder: '10',
value: null,
},
{
key: 'notifyTeam',
type: 'checkbox',
label: 'Notify the team',
value: true,
},
]
| Field | Type | Required | Description |
|---|---|---|---|
key |
String |
yes | Identifies this field's value in the object passed to result functions (see below). |
type |
String |
no | 'text' (default), 'number', or 'checkbox'. |
label |
String |
no | Shown above the input, or next to the checkbox. |
placeholder |
String |
no | Placeholder for text/number fields. |
value |
any |
no | Initial value. Defaults to false for checkboxes, '' otherwise. |
Fields are plain, unvalidated native <input> elements — a type="number" field's value is still a string (or '') exactly like a native HTML input, so convert it yourself (Number(value)) when building your result, same as you would with any form.
Text/number fields render as an outlined box with the label notched into the top border (a native <fieldset>/<legend>, so it works in any browser with no JS). Checkboxes render as a full-width pill: grey with the label in dark text while unchecked, filled with the primary blue and white text once checked, with a small checkbox indicator on the right.
Button object
| Field | Type | Required | Description |
|---|---|---|---|
text |
String |
yes | Button label. |
result |
any | (fieldValues, accumulated) => any |
one of result/goto |
Value the promise resolves with when this button is activated. Can be a plain value, or a function receiving the current fields values and any accumulated data from earlier steps (see "Multi-step dialogs" below) and returning the value to resolve with. |
goto |
{ buttons: Array<Button>, merge?: Object } |
one of result/goto |
Instead of resolving, switches the dialog to a different set of buttons — see "Multi-step dialogs" below. |
color |
String |
no | Color applied to the button text (see "Named colors" above). Defaults to a neutral dark text color if omitted. |
shortcut |
String |
no | Keyboard shortcut that activates this button while the dialog is open (see below). |
Button text is always a single line — long labels are truncated with an ellipsis rather than wrapping. Hovering (or focusing) a truncated button scrolls its text left and back so the full label can be read, then reverts to the ellipsis when you move away. This is automatic and needs no configuration; it only activates when the text actually overflows.
Multi-step dialogs
A button can transition to a different button set instead of resolving, by giving it goto instead of result. This is plain JS composition — no separate "steps" registry to learn, you just reference another array of buttons (your own variable, defined wherever is convenient):
const step2Buttons = [
{
text: 'Normal',
color: 'success',
result: (fields, acc) => ({ mode: acc.mode, replaceBackup: false }),
},
{
text: 'Force backup',
color: 'warning',
result: (fields, acc) => ({ mode: acc.mode, replaceBackup: true }),
},
{ text: 'Back', color: 'error', goto: { buttons: null } }, // patched below
]
const step1Buttons = [
{
text: 'Yes',
color: 'success',
goto: { merge: { mode: 'yes' }, buttons: step2Buttons },
},
{
text: 'Light',
color: 'success',
goto: { merge: { mode: 'light' }, buttons: step2Buttons },
},
{ text: 'No', color: 'error', result: null },
]
step2Buttons[2].goto.buttons = step1Buttons // "Back" returns to step 1
const result = await this.$confirmationBox({
title: 'Proceed with file comparison?',
fields: [
{
key: 'expandSeconds',
type: 'number',
label: 'Expand (seconds)',
placeholder: '10',
},
],
buttons: step1Buttons,
})
goto.buttonsreplaces the dialog's current buttons — the message, icon, title, and fields stay exactly as they were, only the button row changes.goto.mergeshallow-merges into anaccumulatedobject that persists for the whole dialog (reset each time it's opened), passed as the second argument to anyresultfunction — this is how step 1's choice ("Yes" vs "Light") reaches the button you click on step 2.- Fields are always visible and keep their values across steps, since only
buttonschanges. - Keyboard shortcuts on the current step's buttons keep working after a
goto— the listener always reads the live button set.
Keyboard shortcuts
A button's shortcut is matched case-insensitively against KeyboardEvent.code — not .key, so it's layout-independent. Common values: 'Enter', 'Escape', 'Space', 'KeyN' / 'keyn' (both work), 'Digit1', 'ArrowLeft', etc.
buttons: [
{ text: 'Yes', result: true, shortcut: 'Enter' },
{ text: 'No', result: false, shortcut: 'Escape' },
]
The listener is only attached while a dialog is open and is removed as soon as it closes, so shortcuts never leak between dialogs. If Escape isn't bound to a button, pressing it gives a brief "shake" feedback instead of closing the dialog (see below).
Closing by clicking outside
To stop the dialog from closing when the user clicks outside it (or presses Escape), pass clickOutside: null:
await this.$confirmationBox({
message: 'This has to be answered.',
clickOutside: null, // outside click / Escape are blocked; only the buttons can close it
buttons: [...],
})
With that set, clicking outside or pressing Escape (unless it's bound to a button's shortcut) does nothing but give the dialog a brief "shake", so the user notices it's still waiting on them.
Without it, both are still intercepted, just with a different default meaning:
clickOutside: false(the default) — clicking outside closes the dialog and resolves the promise withfalse.clickOutside: null— blocked, as shown above.clickOutside: <anything else>(string/number/object/true) — clicking outside closes the dialog and resolves the promise with that value instead offalse.
clickOutside can be set both as a global install default (see below — handy if you want it blocked app-wide) and overridden per call, consistently.
Global install options
The second argument to Vue.use(ConfirmationBox, options) accepts the same shape as icon, color, title, clickOutside, and width above, and is applied as the default for every dialog in the app, unless overridden per call:
Vue.use(ConfirmationBox, {
icon: 'mdi-alert',
color: 'warning',
title: 'Please confirm',
clickOutside: null, // block closing on outside click / Escape app-wide
width: 400,
})
message and buttons are not meaningful as global options — every dialog needs its own message/buttons, so set those per call.
Return value / error handling
$confirmationBox(...) always returns a Promise that resolves with the activated button's result (or the clickOutside value). There is currently no code path that causes it to reject — plan your await calls accordingly (no need for a .catch()), and don't rely on rejection to detect a specific state like the dialog being dismissed.
Styling
The dialog is rendered with these classes, if you need to override anything from your own global CSS:
.confirmation-box-overlay— the full-viewport backdrop..confirmation-box— the dialog card itself (background, border-radius, shadow, width)..confirmation-box__body— icon/title/message wrapper..confirmation-box__icon— the icon element..confirmation-box__title— the<h3>heading..confirmation-box__fields— the fields wrapper (only present whenfieldsis used)..confirmation-box__field— wrapper around one field..confirmation-box__field-outline,.confirmation-box__input— the outlined box and input for text/number fields (the box's<legend>isn't a custom class, just a plainlegendelement inside it)..confirmation-box__checkbox,.confirmation-box__checkbox--checked,.confirmation-box__checkbox-label,.confirmation-box__checkbox-input— the checkbox pill, its checked-state modifier, the label text, and the checkbox indicator..confirmation-box__actions— the button row..confirmation-box__button— each button..confirmation-box__button-text— the label inside each button (the ellipsis/hover-scroll element).
These styles are shipped as plain global CSS (not scoped), exactly like before, so overriding them from your app's stylesheet works as expected.
Development
npm install # install dependencies
npm run dev # start the Vite dev server against the example app in example/
npm run build # build the library to lib/ (ES + UMD, CSS injected at runtime)
npm run serve # locally preview the production build
The example/ folder is a demo app used purely for manually exercising the component during development — it still uses Vuetify for its own unrelated page chrome (an app bar and a button), which has nothing to do with the library itself; it's not published.
License
MIT