<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.Point any HTML form at one endpoint. 500 free submissions — no card, no time limit — and unlimited forms.
Create free accountWhy 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 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().