Using requestSubmit() to Trigger Validation

Why does a form submitted by a keyboard shortcut, an auto-save timer or a custom button skip every validation rule, never fire your submit handler, and ignore which button was “pressed”? Because it calls form.submit(), which bypasses the entire submission lifecycle. form.requestSubmit() is its well-behaved sibling: it runs constraint validation (unless the form has novalidate), fires the submit event so your handlers run, respects event.preventDefault(), and accepts a submitter button so formaction, formmethod and the button’s name/value apply. This recipe replaces submit() with requestSubmit() in the places script submits forms, and shows how it fits the site’s canonical <form novalidate> plus manual reportValidity() pattern from the Constraint Validation API.

When to Use requestSubmit()

Use it whenever script, rather than a user click on a submit button, starts a submission:

  • Keyboard shortcuts — Ctrl/Cmd+Enter to send a message or save a form.
  • Custom controls — a card-based “Choose this plan” UI that submits the form.
  • Auto-submit flows — submitting when a one-time code is complete or when a filter changes.
  • Multi-button forms — “Save draft” versus “Publish”, where the server needs to know which was chosen.

form.submit() still has a narrow legitimate use: after your own handler has validated everything and you deliberately want a native navigation without re-running handlers, as in the final step of several recipes on this site. Everywhere else, requestSubmit() is the right call. The full lifecycle is described in the form submission lifecycle.

form.submit() versus form.requestSubmit() Two columns comparing what form.submit and form.requestSubmit do regarding validation, the submit event, preventDefault and submitter buttons. form.submit() ✗ no constraint validation ✗ no submit event, handlers skipped ✗ cannot be cancelled ✗ no submitter: formaction and button value ignored form.requestSubmit(button?) ✓ constraint validation (unless novalidate) ✓ fires submit, handlers run ✓ preventDefault() cancels it ✓ submitter supported: formaction, name/value
requestSubmit behaves like a user pressing a submit button; submit skips every step and just sends the form.

Minimal Working requestSubmit Usage

<form id="compose" action="/messages" method="post" novalidate>
  <label for="to">To</label>
  <input id="to" name="to" type="email" required>
  <label for="body">Message</label>
  <textarea id="body" name="body" required minlength="2"></textarea>
  <button type="submit" name="intent" value="draft" formnovalidate>Save draft</button>
  <button type="submit" name="intent" value="send" id="send">Send</button>
  <p class="hint">Press Ctrl+Enter (⌘+Enter on Mac) to send.</p>
</form>
const form = document.querySelector<HTMLFormElement>("#compose")!;
const sendButton = form.querySelector<HTMLButtonElement>("#send")!;

// One submit handler for every way the form can be submitted.
form.addEventListener("submit", (event) => {
  const submitter = event.submitter as HTMLButtonElement | null;
  const isDraft = submitter?.value === "draft";
  // Drafts skip validation (the button has formnovalidate); sending must be valid.
  if (!isDraft && !form.reportValidity()) {
    event.preventDefault();
  }
});

// Keyboard shortcut: submit AS IF the Send button was pressed.
form.addEventListener("keydown", (event) => {
  if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
    event.preventDefault();
    form.requestSubmit(sendButton);        // runs the submit handler with submitter = Send
  }
});

// Auto-save every 30 s as a draft, through the same lifecycle.
const draftButton = form.querySelector<HTMLButtonElement>("button[value=draft]")!;
setInterval(() => {
  if (form.querySelector("textarea")!.value.trim()) form.requestSubmit(draftButton);
}, 30_000);

Passing the submitter is what makes this work: the submit event’s event.submitter is the Send button, so the handler knows to validate, the button’s name=intent value=send pair is included in the submitted data, and any formaction or formmethod on it applies. With the draft button, its formnovalidate attribute is honoured too.

What requestSubmit(submitter) does requestSubmit with a submitter validates the form unless novalidate applies, fires the submit event with event.submitter set, and only submits the form if the event is not cancelled. Script Form Submit handler Network requestSubmit(sendButton) constraint validation skipped: form has novalidate submit event (submitter = Send) reportValidity() fails → preventDefault() submission cancelled; focus on first invalid field
Every step a real button click would trigger happens, in the same order, which is why the same handler serves clicks, shortcuts and timers.

requestSubmit Reference

Aspect Behaviour Notes
Signature form.requestSubmit(submitter?: HTMLElement) Submitter must be a submit button of this form, or it throws
Constraint validation Runs unless novalidate on the form or formnovalidate on the submitter With novalidate, call reportValidity() in your handler
submit event Fired, cancelable event.submitter is the passed button (or null)
Submitter data name/value included in the form data Same as a real click
formaction, formmethod, formenctype, formtarget Honoured Per-button overrides work
form.submit() Skips all of the above Keep for “already validated, just navigate”

Because the site’s forms use novalidate, requestSubmit() does not itself run constraint validation there; it hands control to the submit handler, which calls reportValidity(). That is the point: one handler, one validation path, whatever started the submission.

Verification Steps

import { test, expect } from "@playwright/test";

test("keyboard shortcut validates like the Send button", async ({ page }) => {
  await page.goto("/compose");
  await page.getByLabel("Message").focus();
  await page.keyboard.press("Control+Enter");
  await expect(page.getByLabel("To")).toBeFocused();                  // reportValidity ran
  await page.getByLabel("To").fill("ada@example.com");
  await page.getByLabel("Message").fill("Hello!");
  const [request] = await Promise.all([
    page.waitForRequest("**/messages"),
    page.getByLabel("Message").press("Control+Enter"),
  ]);
  expect(request.postData()).toContain("intent=send");
});

Edge Cases and Failure Modes

Older browsers. requestSubmit arrived in Safari 16. For older engines, a small polyfill clicks a temporary submit button, which triggers the same lifecycle; pass the real submitter when you have one.

if (!HTMLFormElement.prototype.requestSubmit) {
  HTMLFormElement.prototype.requestSubmit = function (submitter?: HTMLElement) {
    if (submitter) { submitter.click(); return; }
    const b = Object.assign(document.createElement("button"), { type: "submit", hidden: true });
    this.append(b); b.click(); b.remove();
  };
}

Submitting from inside the submit handler. Calling requestSubmit() from a submit handler re-enters the handler and can loop. After an async check inside the handler, call form.submit() (you have validated already) or set a flag that lets the next pass through.

Auto-submit on a partial value. Submitting a one-time code form as soon as six digits are typed is convenient, but paste and autofill can deliver the digits in one event. Trigger requestSubmit() from the same input handler for both, and let the submit handler validate — never assume the value is complete.

Double submission. A keyboard shortcut plus a click, or an auto-save during a manual save, can submit twice. Guard in the submit handler, as described in preventing double form submission.

Accessibility of Scripted Submissions

A submission started by a shortcut or timer must be as understandable as one started by a button. Document keyboard shortcuts visibly (the hint in the markup above) and avoid single-key shortcuts without modifiers, which WCAG 2.1.4 Character Key Shortcuts restricts because they fire accidentally for speech-input users. When an auto-save runs, announce it through a polite status region (“Draft saved”) rather than moving focus, and never let an automatic submission trigger reportValidity() — an auto-save that suddenly steals focus to show an error is disorienting. That is another reason drafts use formnovalidate: automatic, background submissions should not validate interactively.

requestSubmit in Framework Code

Frameworks that wrap forms usually listen for the native submit event, which is exactly what requestSubmit() fires — so it is also the correct way to submit a framework-managed form from outside the framework’s own button. React Hook Form’s handleSubmit, VeeValidate’s handleSubmit, SvelteKit’s use:enhance, React Router’s <Form> and Next.js server-action forms all run their validation and pending state when a real submit event arrives, and all are bypassed by form.submit(). A common bug is a “Save” item in an application menu or a keyboard shortcut that calls formRef.current.submit(): the page reloads, the framework’s validation never runs, and server actions are posted without the client-side state they expect. Replacing that call with formRef.current.requestSubmit() restores every framework behaviour with no other change. Where the framework exposes its own programmatic submit (for example a submit() helper returned from a hook), prefer that, since it may also handle framework-specific state; otherwise requestSubmit() is the universal answer.

// React: a global Cmd/Ctrl+S that goes through the form's normal submit path
useEffect(() => {
  const onKey = (e: KeyboardEvent) => {
    if ((e.metaKey || e.ctrlKey) && e.key === "s") {
      e.preventDefault();
      formRef.current?.requestSubmit(saveButtonRef.current ?? undefined);
    }
  };
  window.addEventListener("keydown", onKey);
  return () => window.removeEventListener("keydown", onKey);
}, []);

Validating Before an Asynchronous Submit

When the submit handler must await something — an asynchronous availability check, a token fetch — it has to cancel the native submission first and resume it later. The resume step is where submit() versus requestSubmit() matters most: calling requestSubmit() would re-enter the handler and re-run the async work, while submit() sends the already-validated form directly. A flag keeps the logic explicit.

let validated = false;

form.addEventListener("submit", async (event) => {
  if (validated) return;                              // second pass: let it through
  event.preventDefault();
  if (!form.reportValidity()) return;
  const ok = await checkUsernameAvailable(form);      // async rule
  if (!ok) return form.reportValidity();              // custom validity was set inside the check
  validated = true;
  form.requestSubmit(event.submitter as HTMLButtonElement | undefined);   // keeps submitter data
  validated = false;
});

Here requestSubmit() is used for the resume because it preserves the submitter’s name/value and formaction; the validated flag prevents the loop. The asynchronous checks themselves follow implementing async email availability checks.

Which submit call should this code use? A decision tree for choosing between requestSubmit and submit when script submits a form, based on whether validation and handlers have already run and whether a submitter must be preserved. Have your handlers already validated this submission? no requestSubmit( submitter) yes Do you need the submitter's name, value or formaction? yes requestSubmit( submitter) + re-entry flag no form.submit()
Default to requestSubmit; use submit only when your own handler has already validated and no submitter data is needed.

Frequently Asked Questions

What is the difference between form.submit() and form.requestSubmit()?

submit() sends the form immediately without validation, without firing the submit event and without submitter information. requestSubmit() behaves like clicking a submit button: it validates, fires submit, can be cancelled and supports a submitter.

Does requestSubmit() run validation on a form with novalidate?

No, because novalidate disables native constraint validation. It still fires the submit event, so a handler that calls reportValidity() validates it — which is the site's canonical pattern.

How do I know which button submitted the form?

Read event.submitter in the submit handler. When submitting from script, pass the intended button to requestSubmit(button) so event.submitter is set and its name and value are submitted.

Is requestSubmit() supported everywhere?

It is supported in all current engines (Safari from version 16). For older browsers, a short polyfill clicks the submitter or a temporary hidden submit button.

← Back to The Form Submission Lifecycle