Every app eventually needs one of those "Are you sure?" dialogs. You click delete, and a little box pops up asking you to confirm. Simple enough, but if you've ever wired up that modal state by hand more than twice, you know how tedious it gets. A boolean to track visibility, a callback for confirm, another for cancel, and suddenly your component is drowning in dialog plumbing.
A better approach would be a Promise-based useAlert composable (Vue) or hook (React) that you can call like a regular async function. Call show(...), await the result, and get back true or false. No state management, no prop drilling, just a clean one-liner wherever you need confirmation.
The idea is simple. We create a single alert dialog component that lives at the root of our app. A shared piece of state holds the current dialog data and a resolve function. When you call useAlert().show(...), it sets that state and returns a Promise. The dialog appears, the user clicks OK or Cancel, and the Promise resolves. That's it.
The focus of this post is entirely on the logic — not the styling. We're using plain HTML and basic CSS here for clarity, but you could easily swap in any UI library (like Radix, Headless UI, shadcn/ui, etc.) for the modal itself and the underlying logic would stay exactly the same.
Below is the full implementation in both Vue and React.
<script setup lang="ts">
import { useAlert } from "@/composables/useAlert";
const { current } = useAlert();
const handleConfirm = () => {
current.value?.resolve(true);
current.value = undefined;
}
const handleCancel = () => {
current.value?.resolve(false);
current.value = undefined;
}
</script>
<template>
<div v-if="current !== undefined" class="dialog-backdrop">
<div class="dialog">
<h3 v-if="current.data.title">{{ current.data.title }}</h3>
<p>{{ current.data.message }}</p>
<div class="dialog-actions">
<button class="dialog-btn" @click="handleCancel">
{{ current.data.cancelText || 'Cancel' }}
</button>
<button class="dialog-btn dialog-btn-primary" @click="handleConfirm">
{{ current.data.confirmText || 'OK' }}
</button>
</div>
</div>
</div>
</template>
<style scoped>
.dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2);
padding: 2rem;
min-width: 320px;
max-width: 90vw;
}
.dialog-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
justify-content: flex-end;
}
.dialog-btn {
padding: 0.5rem 1.5rem;
border-radius: 4px;
border: none;
background: #eee;
cursor: pointer;
font-size: 1rem;
}
.dialog-btn-primary {
background: #007bff;
color: #fff;
}
</style>
import { AlertContext, type AlertDialogProps } from "@/context/alert";
import { useState } from "react";
import "./AlertDialog.css";
export const AlertProvider = ({ children }: { children: React.ReactNode }) => {
const [current, setCurrent] = useState<AlertDialogProps | null>(null);
const show = (data: AlertDialogProps["data"]) => {
return new Promise<boolean>((resolve) => {
setCurrent({ data, resolve });
});
};
const handleCancel = () => {
current?.resolve(false);
setCurrent(null);
};
const handleConfirm = () => {
current?.resolve(true);
setCurrent(null);
};
return (
<AlertContext.Provider value={{ show }}>
{children}
{current && (
<div className="dialog-backdrop">
<div className="dialog">
{current.data.title && <h3>{current.data.title}</h3>}
<p>{current.data.message}</p>
<div className="dialog-actions">
<button className="dialog-btn" onClick={handleCancel}>
{current.data.cancelText || "Cancel"}
</button>
<button className="dialog-btn dialog-btn-primary" onClick={handleConfirm}>
{current.data.confirmText || "OK"}
</button>
</div>
</div>
</div>
)}
</AlertContext.Provider>
);
};
Mon Jan 05 2026