How to Prompt for Yes or No in JavaScript

Aug 7, 2026·17 min read

confirm(message) shows a modal two-button dialog and returns true if the user accepted it, false otherwise. That is the whole answer for most code. What it will not do is label those buttons Yes and No: the HTML spec gives confirm() a single parameter, the message, and leaves the labels to the browser and the user’s locale. It also returns false without showing anything at all in a cross-origin iframe, in a sandboxed iframe without allow-modals, and when the user has told the browser to ignore further dialogs. For buttons that really say Yes and No, and an answer you can await, you build the dialog yourself with <dialog>, <form method="dialog"> and returnValue.

The short answer: window.confirm()

One function answers this question, and it fits on a line:

if (confirm("Delete this draft? This cannot be undone.")) {
  deleteDraft(draftId);
}

confirm() is a method on window, so the bare call and window.confirm() are the same call inside a page. It takes the message, shows a modal dialog with two buttons, and hands back a boolean. MDN describes the return as a boolean indicating whether OK (true) or Cancel (false) was selected.

confirm(message) returns true if the user accepted, and false in every other case, including cases where no dialog ever appeared. That last clause is not pedantry, and it gets its own section below.

Here is the delete button in full, with both branches:

const button = document.querySelector("#delete-draft");
const status = document.querySelector("#status");

button.addEventListener("click", () => {
  const accepted = confirm('Delete the draft "quick notes"? This cannot be undone.');

  if (accepted) {
    deleteDraft("quick-notes");
    status.textContent = "Draft deleted.";
  } else {
    status.textContent = "Nothing was deleted.";
  }
});

The click handler stops at the confirm() call. The spec’s algorithm tells the browser to “Pause until the user responds either positively or negatively”, so your code stops at that line until the user answers. There is no callback and nothing to await: the next statement runs once the dialog is gone. MDN describes these boxes as modal windows that prevent the user from reaching the rest of the interface until the box is closed, and adds that under some conditions, such as the user switching tabs, the browser may not display a dialog or may not wait for a response.

The three built-in dialogs sit together in Interaction: alert, prompt, confirm, and the whole course is also sold as books, beginning with Part 1: JavaScript Fundamentals.

Why the buttons can’t say “Yes” and “No”

Look at the signature. confirm() takes one parameter, the message, and that is the entire API surface.

The spec’s algorithm normalizes newlines in the message, optionally truncates it, and then says: “Show message to the user, treating U+000A LF as a line break, and ask the user to respond with a positive or negative response.” A positive or negative response. The spec never names the buttons.

So the labels belong to the browser and the user’s locale, not to your code. There is no second argument, no property, no CSS hook. A reader running their browser in another language gets that language’s words for accept and cancel, and you have no say in it.

prompt() has the same shortage. Its two parameters are the message and a default value for the text field, and neither one touches a button.

The pattern holds across the browser’s built-in dialogs. The beforeunload dialog is the same story from the other end: browsers show a generic browser-specified string, and MDN states that this cannot be controlled by the webpage code. The recommended shape there is event.preventDefault(), with event.returnValue set only for legacy support.

So the thing the query asks for is not awkward, it is unavailable. If the buttons have to read Yes and No, you are building the dialog, and the second half of this article builds it.

When confirm() returns false without asking anyone

The spec has a named condition for refusing to show a dialog. When “we cannot show simple dialogs” holds, confirm() returns false and prompt() returns null before anything is drawn.

It holds when:

  • The active sandboxing flag set of the window’s Document has the sandboxed modals flag set. In markup, that is an <iframe sandbox> without allow-modals. The allow-modals keyword is what permits alert(), confirm(), print() and prompt(), and a sandbox attribute with an empty value applies all restrictions. Opening a <dialog> is allowed regardless of the keyword.
  • The window’s origin and its top-level origin are not same origin-domain. A cross-origin iframe qualifies.
  • The event loop’s termination nesting level is nonzero, at the user agent’s option. That counter goes up while a document is being unloaded, so in practice this is a dialog attempted on the way out, and nothing in your code changes it.
  • At the user agent’s discretion, again optionally. The spec gives the example of a user agent that offers the user the option to ignore all modal dialogs, and MDN says it outright: if a browser is ignoring in-page dialogs, then the returned value is always false.

No exception is thrown in any of those cases. There is no error, no second return channel, no flag to read afterwards. Your code receives false and concludes the user said no, when in fact nobody was asked.

the user answeredClicks OKClicks Cancelno dialog is shownSandboxed frameCross-origin framePage unloadingDialogs mutedtruefalse
One path returns true. Five return false, and only one of those is a person saying no.

That makes the phrasing of your question a correctness decision, not a copy decision. Compare these two:

// safe: a suppressed dialog does nothing
if (confirm("Delete this draft?")) {
  deleteDraft(draftId);
}

// unsafe: a suppressed dialog deletes the draft
if (!confirm("Keep this draft?")) {
  deleteDraft(draftId);
}

Both read fine in review. The first one fails closed: no dialog, no true, no delete. The second fails open, and the user who told their browser to stop showing dialogs loses a draft they never got asked about.

prompt() is the wrong tool for a yes/no question

The query says prompt, and prompt() is a real function, so people reach for it. It is the wrong shape for this question.

prompt(message, default) returns a string containing the text entered by the user, or null. Cancel gives you null. Clicking OK with the field empty gives you an empty string, which is a different value that means something different. MDN puts it directly: the result is a string, which means you should sometimes cast the value given by the user.

So you own the parsing, and the parsing has more cases than you expect:

function parseYesNo(answer) {
  if (answer === null) return false; // Cancel
  const cleaned = answer.trim().toLowerCase();
  return cleaned === "y" || cleaned === "yes";
}

console.log(parseYesNo(null));
console.log(parseYesNo(""));
console.log(parseYesNo("  YES "));
console.log(parseYesNo("yeah"));
false
false
true
false

null is Cancel and "" is OK with an empty field. Both come back false, which is the safe default from the previous section. "yeah" comes back false too, and that is the real problem: you have invented a small language and the user does not have its grammar.

prompt() earns its place when you want text, a name or a quantity, and you convert what comes back yourself; Type Conversions covers that side. For a yes/no question, MDN’s own prompt() page sends you elsewhere: alternatively, a <dialog> element can be used for confirmations.

Building a real yes/no dialog with <dialog>

<dialog> is Baseline Widely available and has been available across browsers since March 2022. Support landed in Chrome 37, Edge 79, Firefox 98 and Safari 15.4. And a dialog you build is page content, not a browser dialog, so the suppression rules above do not apply to it: it still opens in a cross-origin iframe, in a sandbox without allow-modals, and when the user has told the browser to ignore dialogs. That is the second reason to build one, not just the labels.

Two pieces do the work. showModal() displays the dialog in the top layer along with a ::backdrop pseudo-element, and makes every element in the same document, except the dialog and its descendants, inert. A <form method="dialog"> inside the dialog closes it on submit and sets returnValue to the value of the button that was activated.

That second piece is the whole trick: value="yes" and value="no" carry the answer, and the labels are text you typed.

Here is the markup, with the accessible name already wired up:

<dialog id="confirm-dialog" aria-labelledby="confirm-title">
  <h2 id="confirm-title">Delete this draft?</h2>
  <p id="confirm-message">This cannot be undone.</p>
  <form method="dialog">
    <button value="no" autofocus>No</button>
    <button value="yes">Yes</button>
  </form>
</dialog>

aria-labelledby points at the heading, so the dialog has a name. autofocus sits on No, so the first thing focused is the harmless choice. Neither button needs a type: inside a method="dialog" form, activating either one closes the dialog and writes its value into returnValue.

The JavaScript is short:

const dialog = document.querySelector("#confirm-dialog");

document.querySelector("#delete-draft").addEventListener("click", () => {
  dialog.returnValue = "";
  dialog.showModal();
});

dialog.addEventListener("close", () => {
  if (dialog.returnValue === "yes") {
    deleteDraft(draftId);
  }
});

The close event fires when the dialog has closed, whether that was close(), a method="dialog" form submit, or Esc. That covers every exit path, so one listener catches them all. It does not bubble, so that listener has to sit on the dialog itself, and it is not cancelable.

You can also answer from code. close() takes an optional string and sets returnValue to it, so dialog.close("no") from a timeout ends the question the same way the No button would.

Two things to avoid. Do not open the dialog with the open attribute: MDN recommends show() or showModal() instead, and a dialog opened via open is non-modal, which is not what a delete confirmation wants. And showModal() throws an InvalidStateError if the dialog is already open non-modally, which is the failure you get from mixing show() and showModal() on the same element.

Making it awaitable: an askYesNo() helper

The close event is a callback, and a yes/no question wants a value. Wrap it once and the call site reads like confirm():

const dialog = document.querySelector("#confirm-dialog");
const titleEl = document.querySelector("#confirm-title");
const messageEl = document.querySelector("#confirm-message");

function askYesNo(title, message) {
  titleEl.textContent = title;
  messageEl.textContent = message;

  dialog.returnValue = ""; // (1)

  const answer = new Promise((resolve) => {
    dialog.addEventListener(
      "close",
      () => resolve(dialog.returnValue === "yes"), // (2)
      { once: true }, // (3)
    );
  });

  dialog.showModal(); // (4)
  return answer;
}

Using it:

document.querySelector("#delete-draft").addEventListener("click", async () => {
  const confirmed = await askYesNo("Delete this draft?", "This cannot be undone.");

  if (confirmed) {
    deleteDraft(draftId);
  }
});
  1. Resetting returnValue before every open is the load-bearing line. returnValue defaults to an empty string, and after that it keeps whatever the last close wrote. Without the reset, a stale "yes" from the previous answer is still sitting on the element.
  2. Compare against "yes", never against "no".
  3. { once: true } removes the listener after it fires. Without it, every call stacks another handler on the same dialog, and every one of those old handlers runs again on every later close: a leak, and any side effects inside them repeat. The promises they closed over are already settled, so they keep their original answers, because resolve() on a settled promise does nothing.
  4. showModal() last, after the listener is registered, so the code reads in the order it runs.

Point 2 is where the naive version breaks. MDN is explicit: if the user dismisses the dialog without clicking a button, for example by pressing Esc, then the return value is not set. So an Esc dismissal leaves returnValue at whatever it already was, and on a freshly reset dialog that is "":

function decide(returnValue) {
  return { strict: returnValue === "yes", loose: returnValue !== "no" };
}

console.log(JSON.stringify(decide("yes")));
console.log(JSON.stringify(decide("no")));
console.log(JSON.stringify(decide("")));
{"strict":true,"loose":true}
{"strict":false,"loose":false}
{"strict":false,"loose":true}

strict is what askYesNo() does. loose is the bug: on the third line, the Esc case, it reports true and your delete runs because the user tried to get out of the dialog. Drop the reset from step 1 and the strict version breaks the same way, since a leftover "yes" survives the dismissal.

Esc closes without writing returnValuethe handler then reads returnValue === “yes”reset to "" before showModal()returnValue""Escfalsedraft is safeno reset, after an earlier YesreturnValue”yes”Esctruedraft deleted
Esc writes nothing, so the answer you read is whatever the last open left in the slot.

Esc is a close request, and a close request fires a cancelable cancel event before the dialog closes. Calling preventDefault() in a cancel handler keeps the dialog open, and the close event never fires. requestClose() runs the same path from code, while close() cannot be cancelled. requestClose() is Baseline newly available (May 2025), so it is far newer than close() and the rest of the element; check support before you rely on it.

For control over dismissal itself there is the closedby attribute, with three values: any (dismissible by any of the three methods), closerequest (a platform-specific user action or a developer-specified mechanism) or none (developer mechanism only). With no valid value set, a dialog opened via showModal() behaves as closerequest, and otherwise as none. It shipped in Chrome 134 and Edge 134 in March 2025 and Firefox 141 in July 2025, is not Baseline, and as of August 2026 is not in released Safari, though it is in Safari Technology Preview, so check caniuse before you depend on it.

Getting the accessibility right

showModal() buys you most of this. Dialogs opened that way are exposed with aria-modal="true", while those opened with show() or the open attribute are exposed as aria-modal="false". The browser handles the Esc close request and makes the rest of the document inert, so you are not writing a focus trap.

One scoping detail: MDN notes that only the containing document is affected, so a dialog rendered inside an iframe leaves the rest of the page interactive.

What the element does not give you is a name. <dialog aria-labelledby="confirm-title"> pointing at the heading is why the markup above carries that attribute rather than adding it later, and it is what a screen reader announces when the dialog opens.

The other thing you owe it is autofocus. MDN advises putting it on the element the user is expected to interact with immediately, and for a destructive confirmation that element is the safe one:

<form method="dialog">
  <button value="no" autofocus>No</button>
  <button value="yes">Yes</button>
</form>

A user who hits Enter out of habit, or who mashes the keyboard while the dialog appears, cancels. They do not delete. This is the same reasoning as the fail-closed phrasing from earlier: pick the default that costs nothing when it fires by accident.

Yes/no outside the browser: Node, Deno and Bun

The three runtimes answer this differently, and one of them has no answer at all.

Deno ships a global confirm(message?: string): boolean. Two details matter. Only y and Y are treated as true, so yes typed in full comes back false. And it returns false when stdin is not interactive, which is the same fail-quiet shape as the browser’s suppressed dialog: in CI, every question is answered No without anyone being asked. Deno’s prompt(message?, defaultValue?) returns null under the same non-interactive condition.

Bun lists alert, confirm and prompt among its supported globals, noting for each that it is intended for command-line tools. Its reference stops at the signature, confirm(message?: string): boolean, and links out to MDN: which answers count as yes, and what a non-interactive stdin returns, are not documented, so do not assume Deno’s rules carry over.

Node has neither. There is no global confirm() and no global prompt(). You use the promise-based readline API in node:readline/promises, added in v17.0.0 and marked stable in v24.0.0 and v22.17.0:

import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

const rl = readline.createInterface({ input, output });

const answer = await rl.question("Delete this draft? (y/N) ");
rl.close();

if (answer.trim().toLowerCase() === "y") {
  deleteDraft(draftId);
}

rl.question(query[, options]) returns a promise fulfilled with what the user typed, and its options object accepts an AbortSignal, so a question can be cancelled on a timeout rather than hanging a script forever. The parsing is yours again, exactly as it was with prompt() in the browser, which is why the (y/N) in the query text does real work: it tells the user which answer the capital letter is, and the code above defaults every other input to no.

Frequently asked questions

How do I change the confirm() buttons to say Yes and No?
You cannot. The HTML specification gives confirm() exactly one parameter, the message, and its algorithm only asks the user agent to solicit a positive or negative response without ever naming the buttons. The labels come from the browser and the user's locale. If the words Yes and No are a requirement, build the dialog yourself with a <dialog> element and two buttons whose labels you write.
Can confirm() return false without the user clicking anything?
Yes. The spec's 'cannot show simple dialogs' condition makes confirm() return false, and prompt() return null, before anything is displayed. It applies when the window is not same origin-domain with its top-level origin, when the document has the sandboxed modals flag set, and optionally at the browser's discretion, such as when the user has chosen to ignore further dialogs. No exception is thrown, so your code cannot tell this apart from a real No.
Should I use prompt() to ask a yes/no question?
No. prompt() returns the string the user typed, an empty string if they click OK with the field blank, and null if they cancel, which leaves you parsing case, whitespace and every variation of 'y'. Use it when you actually want text. MDN's own prompt() page points at the <dialog> element for confirmations.
How do I await a yes/no dialog in JavaScript?
Wrap a <dialog> in a Promise: reset returnValue, register a close listener with { once: true } that resolves the promise, then call showModal(). Put the answer in the buttons with value="yes" and value="no" inside a <form method="dialog">, then resolve with dialog.returnValue === "yes". Compare against "yes" rather than against "no", because Esc closes the dialog without setting returnValue at all.
Is there a confirm() in Node.js?
Node has no global confirm() or prompt(). Use the promise-based readline API in node:readline/promises, added in v17.0.0 and marked stable in v24.0.0 and v22.17.0, and await rl.question(), which can also take an AbortSignal. Deno and Bun both ship global confirm() and prompt() functions intended for command-line use.