"Double-click to submit" pattern with Stimulus
Inspired by a confirm-by-double-clicking pattern seen in CasaOS, this post recreates it with a small Stimulus controller. It swaps the button text on first click and submits on the second, then adds stimulus-use's clickOutside to cancel the pending confirmation.
While tinkering around with a home lab and CasaOS, I encountered this pattern where certain actions require a “double click”, like this:

Let’s start with a controller:
import { Controller } from '@hotwired/stimulus';
// Connects to data-controller="double-confirm"export default class extends Controller { static targets = ['text']; // The text we want to switch static values = { clicked: Boolean };
connect() { this.clickedValue = false; this.originalText = this.textTarget.innerText; }
submit(event) { event.preventDefault();
if (this.clickedValue) { this.element.requestSubmit(); } else { this.clickedValue = true; this.textTarget.innerText = 'Are you sure?'; } }}Upon submission, we first prevent the default behavior so we can either swap the text or submit the form.
Now, connecting it to our form:
<%= form_with url: user_path(user), method: :delete, data: { controller: "double-confirm" } do |form| %> <% form.button type: :submit, data: { action: "click->double-confirm#submit" }, class: "button success" do %> <span data-double-confirm-target="text">Delete</span> <% end %><% end %>And that’s almost it. There is no way to cancel the action. Once clicked, there’s no coming back; you either have to submit the form by clicking it again or refresh the page so that the button resets to its initial state.
Let’s go with a simple solution: cancel the action whenever the user clicks outside the button. We start by adding stimulus-use:
yarn add stimulus-use @hotwired/stimulusand then we make the controller aware of any clicks outside its element:
import { Controller } from '@hotwired/stimulus'++ import { useClickOutside } from 'stimulus-use'
// Connects to data-controller="double-confirm"export default class extends Controller { static targets = ['text'] static values = { clicked: Boolean }
connect () { this.clickedValue = false this.originalText = this.textTarget.innerText++ useClickOutside(this, { element: this.element }) }
submit (event) { event.preventDefault()
if (this.clickedValue) { this.element.requestSubmit() } else { this.clickedValue = true this.textTarget.innerText = 'Are you sure?' } }
++ clickOutside (event) {++ if (this.clickedValue) {++ this.clickedValue = false++ this.textTarget.innerText = this.originalText++ }++ }}And this is the final result
