Beyond data-confirm
Spice up your UI with beautiful confirmation modals
Accompanying repo: ftes/spice-up-phoenix-data-confirm
Have you ever cringed at those plain, browser-native confirmation dialogs that appear when users click a delete button? You know the ones - functional but certainly not winning any design awards. In this post, I'll show you how to replace those default browser prompts with sleek, styled modals that match your application's design system, all using Phoenix LiveView's data-confirm
attributes.
Before:

After:

The Problem with Browser Confirm Dialogs
By default, Phoenix's phx-click
and link
components support a handy data-confirm
attribute that triggers a browser confirmation dialog when clicked:
<.link
href={~p"/users/#{user}/delete"}
data-confirm="Are you sure you want to delete this user?"
>
Delete User
</.link>
While this works, it comes with several limitations:
- Inconsistent styling - The browser dialog doesn't match your application's design
- Limited customization - No title, no buttons, no icon, no HTML
- Browser variations - Different browsers display these prompts differently
- Accessibility issues - Limited keyboard navigation and screen reader support
Introducing Custom Confirm Modals
Let's create a solution that overrides the default behavior and displays a beautifully styled modal you probably have in your components already instead, while preserving the simplicity of the data-confirm
attribute.
Our solution consists of two parts:
- A modal template in your layout file
- A JavaScript file to handle the interception and display logic
It is based on the LiveView docs on "Overriding the default confirm behaviour".
The key difference is that it ingegrates nicely with your existing Phoenix components like <.modal>
and <.button>
.
Step 1: Add the Modal Template
First, add this modal template to end of your app.html.heex
layout.
This assumes you have some <.modal>
component in your code base already.
<!-- app.html.heex -->
<.modal id="data-confirm-modal" phx-update="ignore" on_cancel={JS.dispatch("data-confirm:cancel")}>
<.icon id="data-confirm-icon" data-class="size-10 hidden" name="hero-ignoreme" />
<h2 id="data-confirm-title"></h2>
<p id="data-confirm-message"></p>
<div>
<.button
:for={variant <- ~w(primary danger)}
id={"data-confirm-button-#{variant}"}
variant={variant}
phx-click={JS.dispatch("data-confirm:confirm")}
>
</.button>
<.button variant="outline" phx-click={JS.dispatch("data-confirm:cancel")}>Cancel</.button>
</div>
</.modal>
This template creates a modal that:
- Has a customizable icon
- Shows a title and message
- Includes primary and danger action buttons (only one of these will show at any given time)
- Has a cancel button
Step 2: Create the JavaScript Handler
Next, create a file called data-confirm.js
in your JavaScript assets directory:
// data-confirm.js
// Store confirmation state since modals don't block execution like window.confirm()
const resolvedAttr = "data-confirm-resolved";
const getEl = (suffix) => document.getElementById(`data-confirm-${suffix}`);
let target = null;
document.body.addEventListener(
"phoenix.link.click",
function (e) {
e.stopPropagation();
const message = e.target.getAttribute("data-confirm");
if (!message) {
return;
}
target = e.target;
if (e.target?.hasAttribute(resolvedAttr)) {
e.target.removeAttribute(resolvedAttr);
return;
}
e.preventDefault();
e.target?.setAttribute(resolvedAttr, "");
populateModal(e.target.dataset);
window.liveSocket.execJS(getEl("modal"), getEl("modal").dataset.show);
},
false,
);
window.addEventListener("data-confirm:confirm", () => {
window.liveSocket.execJS(getEl("modal"), getEl("modal").dataset.hide);
target?.click();
target = null;
});
window.addEventListener("data-confirm:cancel", () => {
window.liveSocket.execJS(getEl("modal"), getEl("modal").dataset.hide);
target?.removeAttribute(resolvedAttr);
target = null;
});
function populateModal(dataset) {
const icon = dataset.confirmIcon;
getEl("icon").className = getEl("icon").dataset.class;
getEl("icon").classList.toggle("hidden", !icon);
if (icon) {
getEl("icon").classList.add(icon);
}
const variant = dataset.confirmVariant || "primary";
["primary", "danger"].forEach((v) =>
getEl(`button-${v}`).classList.toggle("hidden", v !== variant),
);
getEl("title").innerHTML = dataset.confirmTitle || "Are you sure?";
getEl("message").innerHTML = dataset.confirm;
getEl(`button-${variant}`).innerHTML = dataset.confirmButton || "Yes";
}
This JavaScript file:
- Listens for
phoenix.link.click
events - Intercepts clicks on elements with the
data-confirm
attribute - Populates and displays our custom modal
- Handles confirmation or cancellation
Step 3: Import the JavaScript
Make sure to import this file in your main JavaScript entry point:
// app.js
import "./data-confirm"
Step 4: See it in action (commit f008ee)
<.link
phx-click="delete-user"
data-confirm="This action cannot be undone."
>
Delete User
</.link>

When a user clicks "Delete user", a styled confirmation modal is shown.
Only if they confirm, is the action (phx-click
in this case) executed.
How It Works
Our implementation works by:
- Intercepting the default behavior - We listen for clicks before Phoenix's built-in handler
- Showing a custom modal - Instead of using
window.confirm()
- Tracking confirmation state - Using a custom attribute to remember if a user confirmed
- Re-triggering the original action - Only after confirmation
Enhanced Customization Options (commit 6fbb60)
The real beauty of this approach is the additional customization options:
<.link
href={~p"/users/#{user}/delete"}
data-confirm="This action <b>cannot</b> be undone."
data-confirm-title="Delete this user account?"
data-confirm-button="Delete Account"
data-confirm-variant="danger"
data-confirm-icon="hero-exclamation-triangle"
>
Delete User
</.link>

These additional attributes allow you to:
- Set a custom title - With
data-confirm-title
- Customize the action button - With
data-confirm-button
- Change the button style - With
data-confirm-variant
(commit 90bc03) - Add an icon - With
data-confirm-icon
(using your icon library) - Styled modal content - With any of the
data-confir-*
attributes you can use HTML tags
Benefits
This approach offers several significant advantages:
- Design consistency - Modals match your application's UI
- Enhanced UX - Better spacing, more descriptive options
- Accessibility improvements - Proper focus management and semantic HTML
- Flexible customization - Different types of confirmation for different actions
- Implementation simplicity - Still uses the familiar
data-confirm
attribute
Potential Extensions
You could extend this pattern further:
- Add more button variants
- Implement additional modal templates for different confirmation types
- Trigger the confirmation modal from the server (e.g. after a validation round trip) using a hidden link and an aditional JS event handler
Conclusion
With just a small amount of code, we've transformed the user experience of confirmation dialogs in our Phoenix LiveView application. Instead of jarring browser-native prompts, users now see beautifully styled modals that match our design system.
The implementation maintains the simplicity of the original data-confirm
API while adding powerful customization options. Best of all, it works seamlessly with both standard links and LiveView's phx-click elements.
Give your users a more polished experience by spicing up those confirmation dialogs!