splitforms.com
← Back to the journal

Disable the Submit Button Until the Form Is Valid (JavaScript)

Enable the submit button only when required fields are valid with checkValidity(), handle autofill, use aria-disabled for accessibility, and React.

Start free — 500 submissionsSee pricing →No card, no time limit. Pro is $5 per month.
Short answer
Start the button with disabled, then on every input and change run btn.disabled = !form.checkValidity(). Run it once on load for autofill. Because disabled buttons cannot be focused and give no reason, consider keeping the button enabled with aria-disabled="true" and calling reportValidity() on click instead.
Try it on your own form

Point any HTML form at one endpoint. 500 free submissions — no card, no time limit — and unlimited forms.

Create free account

Quick answer: checkValidity() on input

Let the browser's built-in constraints decide what "valid" means. Mark fields with required, type="email" and minlength, put the disabled attribute on the <button> so it starts off, and flip it whenever the user types:

<form id="contact" action="/api/contact" method="post">
  <label for="name">Name</label>
  <input id="name" name="name" required minlength="2" />

  <label for="email">Email</label>
  <input id="email" name="email" type="email" required />

  <label for="message">Message</label>
  <textarea id="message" name="message" required minlength="10"></textarea>

  <button type="submit" disabled>Send</button>
</form>

<script>
  const form = document.getElementById("contact");
  const btn = form.querySelector("button[type=submit]");

  form.addEventListener("input", () => {
    btn.disabled = !form.checkValidity();
  });
</script>

form.checkValidity() returns true only when every control passes its constraints, so the button turns on the moment the last field becomes valid and turns off again if the user clears one. The input event bubbles, so one listener on the form covers every field. For a full tour of the Constraint Validation API, see how to validate an HTML form with JavaScript.

One side effect: checkValidity() fires the invalid event on each failing field. If you use invalid listeners to set custom messages (as in changing the HTML5 validation message), check Array.from(form.elements).every((el) => el.validity?.valid ?? true) instead, which reads the state silently.

Disabling the button does not tell users when to show inline errors under each field; for blur vs input vs submit timing, see validate form fields on blur (not on every keystroke).

Why disabling the button can be a bad idea

A greyed-out button is a common pattern, but it has two real costs:

  • Users cannot tell why it is greyed out. If one field has a subtle problem (an email missing its domain, a message one character too short), they are left guessing which field to fix.
  • A disabled button is removed from the tab order and cannot be focused. Screen-reader users tabbing through the form may never reach it, so they may not know a submit button exists at all.

The accessible alternative keeps the button enabled and focusable, marks it with aria-disabled="true" plus visual styling, and explains the problem on click with reportValidity(), which shows the browser's message and focuses the first invalid field:

<button type="submit" aria-disabled="true">Send</button>

<style>
  button[aria-disabled="true"] { opacity: .5; cursor: not-allowed; }
</style>

<script>
  const form = document.getElementById("contact");
  const btn = form.querySelector("button[type=submit]");

  function update() {
    btn.setAttribute("aria-disabled", String(!form.checkValidity()));
  }
  form.addEventListener("input", update);

  form.addEventListener("submit", (e) => {
    if (!form.checkValidity()) {
      e.preventDefault();
      form.reportValidity(); // shows the error and focuses the field
    }
  });
</script>

Users still get the visual "not ready yet" cue, but a click now tells them exactly what is missing instead of doing nothing. If you prefer inline error text over the native bubble, add novalidate and render messages next to each field as shown in the custom message guide.

Handle browser autofill

Browser and password-manager autofill does not always fire input. The fields look filled, but your listener never ran, so the button stays disabled. Run the same check on change and once on page load:

function update() {
  btn.disabled = !form.checkValidity();
}

form.addEventListener("input", update);
form.addEventListener("change", update);  // selects, checkboxes, some autofill
window.addEventListener("load", update);  // values restored or autofilled on load

change also covers selects, checkboxes and radios, which do not always fire input in older browsers. If autofill still slips past, the aria-disabled pattern above is the safest fallback, because the real check happens at click time.

CSS-only hint with :invalid

A form matches :invalid while any of its controls fails validation, so you can dim the button with no JavaScript at all:

form:invalid button[type="submit"] {
  opacity: .5;
}

This is styling only. The button is still clickable, and clicking it just triggers the browser's normal validation bubbles, which is often exactly the behavior you want.

React: disable the button until the form is valid

In React, derive isValid from your state or ask the DOM with a ref, and combine it with your sending flag:

function ContactForm() {
  const formRef = useRef<HTMLFormElement>(null);
  const [isValid, setIsValid] = useState(false);
  const [sending, setSending] = useState(false);

  return (
    <form
      ref={formRef}
      onChange={() => setIsValid(formRef.current?.checkValidity() ?? false)}
      onSubmit={async (e) => {
        e.preventDefault();
        setSending(true);
        await fetch("/api/contact", { method: "POST", body: new FormData(e.currentTarget) });
        setSending(false);
      }}
    >
      <input name="email" type="email" required />
      <textarea name="message" required minLength={10} />
      <button type="submit" disabled={!isValid || sending}>
        {sending ? "Sending…" : "Send"}
      </button>
    </form>
  );
}

React's onChange fires on every keystroke, like the native input event. With React Hook Form, call useForm({ mode: "onChange" }) and use formState.isValid: <button disabled={!formState.isValid || formState.isSubmitting}>. The sending half of the condition stops double clicks while the request is in flight; that part is covered in depth in preventing duplicate form submissions.

Still validate on the server

A disabled button is a UX hint, not a security check. Anyone can remove the attribute in DevTools or post straight to your endpoint, and bots never load your JavaScript. Validate every field again on the server. Paste your markup into the form validator to catch missing names and broken constraints, or sign up free for a splitforms endpoint that handles submissions for you.

FAQ

How do I enable the submit button only when all fields are filled?

Mark the fields with required (plus type and minlength where needed), start the button with the disabled attribute, then listen for input and change on the form and set button.disabled = !form.checkValidity(). Run the same check once on page load so autofilled forms are not stuck disabled.

Is disabling the submit button bad for accessibility?

It often is. A disabled button cannot receive keyboard focus, so screen-reader users tabbing through the form may never hear that it exists, and sighted users get no hint about what is missing. A safer pattern keeps the button enabled with aria-disabled="true" and visual styling, then calls reportValidity() on click to show which field needs fixing.

Why is my submit button still disabled after browser autofill?

Some browsers fill saved values without firing an input event, so your check never runs. Listen for change as well, run the check once when the page loads, and optionally again on a short timeout or on focus. Reading form.checkValidity() at click time is the most reliable fallback.

How do I disable a button until the form is valid in React?

Derive an isValid value from your state or from formRef.current?.checkValidity() on every change, then render <button disabled={!isValid || sending}>. With React Hook Form, use useForm({ mode: 'onChange' }) and read formState.isValid. Keep server-side validation either way.

References: MDN: HTMLFormElement.checkValidity() and MDN: aria-disabled.

Related articles

More practical guidance from tutorials.

Browse the journal →
Tutorials

How to Remove the Arrows From a Number Input (CSS, Tailwind, React)

Hide the number input spin buttons with ::-webkit-inner-spin-button and appearance: textfiel…

7 min readRead →
Tutorials

How to Change the Autofill Background Color of an Input (CSS, Tailwind)

Remove Chrome's yellow autofill background with input:-webkit-autofill, inset box-shadow, -w…

7 min readRead →
Tutorials

input type=number maxlength Not Working? 4 Fixes That Work

Why maxlength is ignored on input type=number, plus four fixes: min/max, oninput slice, text…

8 min readRead →

Explore this topic

Start with the overview, then move into focused guides.

OverviewContact Forms for Every FrameworkStart here →GuideContact Form for Next.jsRead →GuideContact Form for ReactRead →GuideContact Form for VueRead →ReferenceContact form guideOpen →

Building forms with ChatGPT, Claude, Cursor, or v0? Connect the native MCP server and give your agent a production form backend.

Explore the MCP server →

Give your form a production backend.

One endpoint adds delivery, spam filtering, storage, and integrations. Start with 500 free submissions — no card, no time limit.

Create free accountRead the docs →
Secure checkoutSSL encryptionPrivacyProtected
VISAAMERICANEXPRESSstripe