skip to content

In PHP, why is an unchecked checkbox missing from $_POST, and how do you read it reliably as a boolean?

level: juniorimportance: should knowfreq 55%

answer

  1. only successful controls are submitted
  2. no value attribute means "on"
  3. ?? default instead of a bare read
  4. hidden field with the same name first
  5. empty checkbox group: no key at all

basics

~20 s

Browsers submit a checkbox only when it is checked, sending its value ("on" when none is set), so an unchecked box leaves no key. Read it with isset() or a ?? default, or put a same-named hidden field before it.

solid answer

~40 s

HTML only submits **successful controls**, and an unchecked checkbox is not one, so the pair is never sent and `$_POST` has no key for it; reading `$_POST['newsletter']` directly raises an "Undefined array key" warning. A checked box sends its `value`, or the string `"on"` if it has none. The simple read is `$subscribed = isset($_POST['newsletter']);` or `($_POST['newsletter'] ?? '0') === '1'`. When absence is ambiguous (a partial update form that did not render the box), place `<input type="hidden" name="newsletter" value="0">` **before** the checkbox: unchecked sends only the `0`; checked sends both and the later `1` wins. That trick does not work for `name[]` groups, where both pairs would land in the list. An empty group has no key, so default with `?? []`.

code

php · 16 lines
php
<?php
declare(strict_types=1);

// Markup, in this order:
// <input type="hidden"   name="newsletter" value="0">
// <input type="checkbox" name="newsletter" value="1">
$newsletter = $_POST['newsletter'] ?? null;   // null: box not rendered
if ($newsletter !== null) {
    $subscribed = $newsletter === '1';        // '0' when unchecked
}

// Group: <input type="checkbox" name="topics[]" value="parks"> ...
$topics = $_POST['topics'] ?? [];              // no key when none ticked
if (!is_array($topics)) {
    $topics = [];
}

go deeper

for a junior

Remember that an unchecked checkbox sends nothing, a checked one sends its value or "on", and absent keys need isset() or a ?? default.

for a middle

Explain the hidden-field pattern through the last-pair-wins rule and document order, and why it cannot work for name[] groups.

for a senior

Point out the partial-form hazard: treating absence as false can wipe settings a user never saw, so make the unchecked state explicit.

for a principal

Decide how forms signal field presence across the codebase, so partial updates never guess and every handler follows one convention.

## Why the key is missing When a form is submitted, the browser builds the request body from the form's **successful controls** only. A checkbox is successful only while it is checked. An unchecked checkbox contributes nothing: no name, no empty value, no `false`. PHP can only register pairs that arrive, so `$_POST` simply has no key for it. This surprises people because other empty fields behave differently. An empty text input *is* sent, as `name=`, and arrives as the empty string `""`. The checkbox is the odd one out, together with unselected radio groups, a `<select multiple>` with nothing chosen, and `disabled` controls. ## What a checked box sends A checked checkbox sends its `value` attribute. If the markup has no `value`, the browser sends the literal string `"on"`. So the possible states for `<input type="checkbox" name="newsletter">` are: | Box state | `$_POST['newsletter']` | |---|---| | checked, no `value` attribute | `"on"` | | checked, `value="1"` | `"1"` | | unchecked | key absent | ## Reading it without warnings Reading an absent key directly, `$_POST['newsletter']`, raises an **"Undefined array key"** warning in PHP 8 and yields `null`. The idiomatic reads are: - `isset($_POST['newsletter'])` - true when the box was checked, regardless of its value; - `($_POST['newsletter'] ?? '0') === '1'` - when you also want to confirm the value you rendered; - `$topics = $_POST['topics'] ?? [];` for a group named `topics[]`, which has **no key at all** when nothing is ticked, not an empty array. ## The hidden-field pattern Treating "absent" as "unchecked" has one real weakness: absence also happens when the form never rendered the checkbox, for example a settings page that only shows some options to some users. A handler that saves `false` for every missing box would wipe settings the user never saw. The fix is to make the unchecked state explicit with a hidden input carrying the **same name** placed **before** the checkbox: 1. unchecked - only the hidden pair `newsletter=0` is sent, so the value is `"0"`; 2. checked - both pairs are sent in document order, and PHP's last-pair-wins rule for a repeated plain name leaves `"1"`; 3. not rendered - neither is sent, so the key is absent and the handler can leave the stored setting alone. The ordering matters: put the hidden field after the checkbox and it overwrites the checked value every time. ## Related controls with the same behaviour The checkbox is the best-known case, but several controls share the "absent when empty" rule, and the same reading discipline applies to all of them: | Control | Nothing chosen | Something chosen | |---|---|---| | single checkbox | key absent | its `value`, or `"on"` | | checkbox group `topics[]` | key absent | list of ticked values | | radio group `rating` | key absent | the chosen value | | `<select multiple name="district[]">` | key absent | list of selected values | | plain `<select name="ward">` | no option pre-selected: the first option's value is sent | the selected value | A single-choice `<select>` with options normally sends something, because the browser selects the first option by default; a radio group with no default does not. When a question is required, the handler must treat a missing key as "no answer", not as a zero or an empty string. ## Where the pattern breaks - **Checkbox groups.** With `name="topics[]"`, a hidden `topics[]` pair would be appended to the list instead of overwritten, polluting the answers. For groups, send a separate marker instead (`<input type="hidden" name="topics_present" value="1">`) and treat a missing `topics` key as "none ticked" only when the marker is there. - **Disabled checkboxes.** A `disabled` box is never sent, checked or not; if its state must still reach the server, carry it in a hidden field instead. - **Trusting the value.** A client can post `newsletter=yes` or `newsletter[]=1`. Compare against the exact value you rendered with `===`, and treat anything else as unchecked or invalid; broader validation is a separate step. Nothing here is specific to POST: the same rules hold for a checkbox in a GET form and `$_GET`.

  • Why must the hidden field come before the checkbox and not after it?
    When the box is checked, both pairs share the plain name and PHP keeps the last one it parses. Browsers send pairs in document order, so the hidden `0` must come first for the checkbox's `1` to overwrite it. Placed after, the hidden `0` would win every time and the box could never read as checked.
  • Why does the hidden-field trick fail for a checkbox group named topics[]?
    With brackets, each pair is appended to the list rather than overwriting it. A hidden `topics[]` value would become an extra element, so the handler would see a bogus answer next to the real ones. Use a differently named marker field to tell "no box ticked" apart from "group not rendered".

saying these in an interview costs you the question

  • An unchecked checkbox is sent with an empty value, so the key exists.
  • An unchecked checkbox arrives in $_POST as the boolean false.
  • A checkbox with no value attribute sends an empty string when checked.
  • An unticked group named topics[] arrives as an empty array.
  • The hidden fallback field can go anywhere in the form.