import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["modal", "message", "confirm", "cancel"]
static values = {
message: String
}
show(event) {
// Prevent the default Turbo confirm
event.preventDefault()
const message = event.detail?.message || this.messageValue || "Are you sure?"
this.messageTarget.textContent = message
this.modalTarget.classList.remove("hidden")
// Store the original event to replay it
this.pendingEvent = event
}
proceed() {
this.modalTarget.classList.add("hidden")
// Resume the Turbo request
if (this.pendingEvent) {
this.pendingEvent.detail.resume()
this.pendingEvent = null
}
}
cancel() {
this.modalTarget.classList.add("hidden")
this.pendingEvent = null
}
}
<!DOCTYPE html>
<html>
<head>
<title>MyApp</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body data-controller="confirm" data-action="turbo:before-fetch-request@document->confirm#show">
<%= yield %>
<!-- Custom confirmation modal -->
<div data-confirm-target="modal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 max-w-md mx-4">
<h3 class="text-lg font-semibold mb-4">Confirm Action</h3>
<p data-confirm-target="message" class="text-gray-700 mb-6"></p>
<div class="flex gap-3 justify-end">
<button data-action="confirm#cancel" class="btn btn-secondary">Cancel</button>
<button data-action="confirm#proceed" class="btn btn-danger">Confirm</button>
</div>
</div>
</div>
</body>
</html>
<div class="post">
<h1><%= @post.title %></h1>
<%= simple_format @post.body %>
<% if can? :destroy, @post %>
<%= button_to "Delete Post",
post_path(@post),
method: :delete,
class: "btn btn-danger",
form: {
data: {
turbo_confirm: "This will permanently delete '#{@post.title}'. This action cannot be undone."
}
} %>
<% end %>
</div>
The default browser confirm() dialog is ugly and doesn't match your design system. Turbo provides hooks to intercept confirmation dialogs and show custom modals instead. I listen for the turbo:before-fetch-request event, check if the element has data-turbo-confirm, and prevent the request while showing a styled modal. When the user confirms, I programmatically trigger the original action. This pattern works for delete links, dangerous actions, or any operation requiring explicit confirmation. The custom dialog can include rich HTML, additional context, or async operations before proceeding. I also use this hook for optimistic UI updates that revert if the server request fails.