splitforms.com
← Back to the journal

Send a Value for an Unchecked Checkbox (HTML Hidden Input Trick)

Browsers skip unchecked checkboxes. Send a value anyway with a hidden input placed first, FormData.set() or React, plus PHP, Express and Flask parsing.

Start free — 500 submissionsSee pricing →No card, no time limit. Pro is $5 per month.
Short answer
Browsers only submit checked checkboxes. To send a value when one is unchecked, put <input type="hidden" name="newsletter" value="no"> before the checkbox with the same name and read the last value on the server, or set the value yourself on a FormData object with JavaScript. The cleanest default is to treat a missing key as false.
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

Why an unchecked checkbox sends nothing

The HTML spec builds the form data set from the form's controls, and a checkbox is only added when it is checked. An unchecked box is skipped completely. It doesn't send false, 0 or an empty string. The key is simply missing from the request.

<form method="POST" action="/subscribe">
  <input type="email" name="email" />
  <label>
    <input type="checkbox" name="newsletter" />
    Send me the newsletter
  </label>
  <button>Sign up</button>
</form>

<!-- checked:   email=a%40b.com&newsletter=on -->
<!-- unchecked: email=a%40b.com                -->

Notice the on. A checked checkbox with no value attribute sends the default string "on". Always give it a real value such as value="yes" so your server receives something meaningful.

The same rule applies to unselected radio buttons and to disabled controls: they are left out too. Most of the time that is fine. It only becomes a problem when your backend or spreadsheet expects every field to be present, or when you need to tell "the user said no" apart from "this field was never on the form".

The hidden input trick (and its order rule)

The classic fix needs no JavaScript. Add an input type=hidden with the same name and the "off" value, placed before the checkbox:

<input type="hidden" name="newsletter" value="no" />
<input type="checkbox" name="newsletter" value="yes" />

Now the form always sends something. Unchecked, it sends newsletter=no. Checked, it sends both values in document order: newsletter=no&newsletter=yes. The last value is the real answer, so the hidden input must come first. If you put it after the checkbox, the last value is always no.

That means the trick only works if your server reads the last value for a repeated name. Backends don't agree on this.

PHP: $_POST keeps the last value

PHP overwrites a repeated name as it parses the body, so $_POST already holds the last value. The trick works with no extra code. Don't add [] to the name, or you get an array instead.

<?php
$newsletter = $_POST['newsletter'] ?? 'no'; // "yes" or "no"
$subscribed = $newsletter === 'yes';

Express: urlencoded (qs) gives an array

With express.urlencoded({ extended: true }), a repeated name becomes an array, so a checked box arrives as ['no', 'yes'] and an unchecked one as the string 'no'. Take the last item:

app.use(express.urlencoded({ extended: true }));

app.post("/subscribe", (req, res) => {
  const raw = req.body.newsletter;               // "no" or ["no", "yes"]
  const last = Array.isArray(raw) ? raw.at(-1) : raw;
  const subscribed = last === "yes";
  // ...
});

Flask: request.form.get returns the first value

Flask (Werkzeug) is the trap. request.form.get('newsletter') returns the first value, which is always the hidden no. Use getlist() and take the last item:

from flask import request

@app.post("/subscribe")
def subscribe():
    values = request.form.getlist("newsletter")  # ["no"] or ["no", "yes"]
    subscribed = bool(values) and values[-1] == "yes"
    # ...

The JavaScript way: set the value on FormData

If you already send the form with fetch, skip the hidden input and fix the data before sending. Build a FormData object, then call set() for each checkbox. set() replaces any existing entry, so every checkbox ends up with exactly one value:

form.addEventListener("submit", async (e) => {
  e.preventDefault();
  const data = new FormData(form);

  form.querySelectorAll('input[type="checkbox"][name]').forEach((cb) => {
    data.set(cb.name, cb.checked ? cb.value : "no");
  });

  await fetch(form.action, { method: "POST", body: data });
});

This suits single yes/no checkboxes. Don't use it on a checkbox group that shares one name (like services), because set() would collapse the group into one value. For groups, read data.getAll("services") and send an empty list when nothing is ticked.

React: a controlled checkbox and a JSON boolean

In React the problem goes away if you keep the checkbox in state and send JSON. A boolean is always present, true or false:

function SignupForm() {
  const [email, setEmail] = useState("");
  const [newsletter, setNewsletter] = useState(false);

  async function onSubmit(e) {
    e.preventDefault();
    await fetch("/api/subscribe", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email, newsletter }), // newsletter: true | false
    });
  }

  return (
    <form onSubmit={onSubmit}>
      <input type="email" value={email}
             onChange={(e) => setEmail(e.target.value)} />
      <label>
        <input type="checkbox" checked={newsletter}
               onChange={(e) => setNewsletter(e.target.checked)} />
        Send me the newsletter
      </label>
      <button type="submit">Sign up</button>
    </form>
  );
}

Read e.target.checked, not e.target.value. The value is the fixed string from the value attribute and doesn't change when the box is ticked.

Alternatives: a yes/no radio pair, or treat missing as false

A yes/no radio pair. If the user must actively choose, use two radio buttons with the same name and add required. The form can't be sent until one is picked, and the chosen value is always submitted:

<fieldset>
  <legend>Send me the newsletter?</legend>
  <label><input type="radio" name="newsletter" value="yes" required /> Yes</label>
  <label><input type="radio" name="newsletter" value="no" /> No</label>
</fieldset>

Treat a missing key as false (the recommended default). For an ordinary opt-in checkbox, the simplest and most robust rule lives on the server: if newsletter is in the request, it is checked; if it is missing, it is unchecked. No hidden inputs, no order rule and no special parsing per framework:

// Node
const subscribed = "newsletter" in req.body;

# Python
subscribed = "newsletter" in request.form

// PHP
$subscribed = isset($_POST['newsletter']);

Use the hidden input or JavaScript only when a tool you can't change needs an explicit value for every field, such as a spreadsheet column or a strict API schema.

Send the form to splitforms

Here is a plain HTML form that posts to splitforms with the hidden no before the checkbox. No backend or JavaScript is needed:

<form action="https://splitforms.com/api/submit" method="POST">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />

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

  <input type="hidden" name="newsletter" value="no" />
  <label>
    <input type="checkbox" name="newsletter" value="yes" />
    Send me the newsletter
  </label>

  <button type="submit">Sign up</button>
</form>

splitforms collects repeated non-empty values under one key into an array, so an unchecked box is stored as no and a checked one as ["no", "yes"], where the last item is the answer. If you want a single value instead, use the FormData.set() code above or send JSON with a boolean. The field and payload reference explains how repeated values and checkboxes are stored. Get a free access key to try it.

FAQ

Why does my checkbox send "on"?

A checked checkbox with no value attribute sends the default string on. Add value="yes" (or any string you want) and that string is sent instead. When the box is unchecked, nothing is sent at all, not even on.

How do I send false for an unchecked checkbox?

Put <input type="hidden" name="newsletter" value="no"> before the checkbox with the same name, then read the last value on the server. With JavaScript, call data.set(cb.name, cb.checked ? cb.value : 'no') on a FormData object before you send it. The simplest option is to treat a missing key as false on the server.

Does a disabled checkbox send a value?

No. Disabled controls are never included in a form submission, checked or not. If you need the value sent while the user can't change it, keep the checkbox enabled and add a hidden input with the value, or prevent changes another way and check it on the server.

Why does the hidden input have to come before the checkbox?

Form data is sent in document order. With the hidden input first, a checked box gives newsletter=no&newsletter=yes, so the last value is the real answer. If the hidden input came after the checkbox, the last value would always be no.

Is the hidden input trick safe to use with every backend?

Only if the backend reads the last value. PHP does by default, but Express (qs) gives an array and Flask's request.form.get returns the first value, which would be the hidden no. Check how your server handles repeated names before relying on it.

References: MDN: input type=checkbox and MDN: FormData.set().

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