Skip to content

Confirming an action

A confirmation is an interruption. It is worth it when the action cannot be undone, and only then.

<button class="lt-button" data-variant="danger" id="cancel-job" type="button">
Cancel the job
</button>
<dialog class="lt-dialog" id="confirm" aria-labelledby="confirm-title">
<h2 class="lt-dialog__title" id="confirm-title">Cancel job 222-0481?</h2>
<div class="lt-dialog__body">
<p>The crew keeps the advance. This cannot be undone.</p>
</div>
<form method="dialog" class="lt-dialog__footer">
<button class="lt-button" data-variant="secondary" value="keep">Keep the job</button>
<button class="lt-button" data-variant="danger" value="cancel">Cancel the job</button>
</form>
</dialog>
<lt-toast id="messages" label="Notifications"></lt-toast>
<script type="module">
import { defineLtToast } from
"https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.40.0/dist/components/standalone/toast/lt-toast.js"
defineLtToast()
const dialog = document.getElementById("confirm")
const messages = document.getElementById("messages")
document.getElementById("cancel-job").addEventListener("click", () => {
// Escape and close() without a value leave the previous value in place.
dialog.returnValue = ""
dialog.showModal()
})
dialog.addEventListener("close", async () => {
if (dialog.returnValue !== "cancel") return
await cancelJob()
messages.show("Job 222-0481 has been cancelled", { color: "success" })
})
</script>

showModal(), not open. Only that moves focus in, keeps focus in the dialog, closes on Escape and makes the rest of the page unreachable.

<form method="dialog"> closes the dialog when a button is pressed, and puts the button’s value in dialog.returnValue. You need no listeners on the buttons, and Enter works as expected.

WhereWriteDo not write
The headingThe question: “Cancel job 222-0481?”“Are you sure?”, “Confirm”
The body textWhat happens: “The crew keeps the advance.”“This action cannot be undone.” on its own
The buttonThe action: “Cancel the job”“OK”, “Yes”

The button text matters most. Someone who hears the dialog without seeing the heading hears only the buttons. “OK” says nothing about what is about to happen.

The action is done, and the user needs to know. A toast in the corner is enough for something that went well:

messages.show("Job 222-0481 has been cancelled", { color: "success" })

But the toast disappears. So:

  • The undo button does not belong there. If the action should be undoable, the button belongs in the page, and then perhaps a confirmation was not what you needed.
  • Nor does an error. If the cancellation failed, that is an Alert in the page, or a toast with duration: 0 that stays.
  • Focus has to go somewhere. The browser returns focus to the button that opened the dialog. If that button is gone, because the row it was in has been deleted, you have to move focus yourself, for example to the heading of the list.

If the action can be undone, undo is better than confirmation. An extra click every time costs more than an undo button for the few times someone gets it wrong.

Confirmation is right when:

  • something disappears for good
  • something is sent to others, such as an order or a message
  • something costs money

Confirmation is wrong when the action is merely inconvenient to redo. Then you create a dialog everyone clicks away without reading, and the next time it matters, they do the same.

  • Preventing double clicks. Reject a new press while the action is in progress, and show it with aria-disabled="true". If you switch the button off with disabled, it loses the focus the browser just gave back to it when the dialog closed, and focus ends up on <body>.
  • Remembering the choice. “Don’t ask me again” is a setting your app has to keep.
  • Deciding what is dangerous. data-variant="danger" is a colour. What deserves it is a judgement.