Checkbox Technical Notes

Submission Basics

  • A checked checkbox submits name=VALUE. If you don’t set value, browsers send on.

  • An unchecked checkbox submits nothing.

Change the submitted values

True value

Pick your “true” value with the value attribute:

<input type="checkbox" name="agree" value="1">

False value

To also send a “false” value when unchecked, use one of these:

  1. Hidden fallback (no JS; two controls with same name)

<input type="hidden"  name="agree" value="0">
<label><input type="checkbox" name="agree" value="1"> I agree</label>

Server receives agree=0 when unchecked, agree=0&agree=1 when checked (order = DOM order). If your backend can’t handle duplicate keys, disable the hidden input when checked:

<script>
const cb = document.querySelector('input[type=checkbox][name=agree]');
const hid = document.querySelector('input[type=hidden][name=agree]');
cb.addEventListener('change', () => { hid.disabled = cb.checked; });
</script>
  1. formdata hook (single definitive value)

<form id="f">
  <label><input id="agree" type="checkbox" name="agree" value="1"> I agree</label>
</form>
<script>
const f = document.getElementById('f');
const cb = document.getElementById('agree') as HTMLInputElement;
f.addEventListener('formdata', e => {
  e.formData.set('agree', cb.checked ? '1' : '0'); // always one key
});
</script>
  1. Handle on the server: treat missing key as false.

A11y checklist

  • Provide a real <label> (wrap or for). This is the accessible name and expands the click target.

  • Keep it a native <input type="checkbox">; don’t replace with divs. If styled as a toggle, keep checkbox semantics (no extra role).

  • Don’t rely on color alone for state; ensure a positional/icon change and visible focus outline.

  • Group related boxes with <fieldset><legend>…</legend></fieldset>.

  • Use required, aria-invalid, and an inline error message (referenced with aria-describedby) when applicable.

  • For tri-state use el.indeterminate = true (JS) if needed.

Using <legend>

<fieldset><legend> grouping

  • Purpose: programmatically group related controls (e.g., a set of checkboxes answering one question). Screen readers announce the legend as the group label when navigating within the group.

  • Rules:

    • Put one <legend> as the first child of <fieldset>.

    • Keep a meaningful legend that reads well before each inner control’s label.

    • Use when instructions/errors apply to the whole group.

Example with error/help

<fieldset aria-describedby="pets-hint pets-err">
  <legend>Select your pets</legend>
  <p id="pets-hint">Choose all that apply.</p>

  <label><input type="checkbox" name="pets" value="dog"> Dog</label>
  <label><input type="checkbox" name="pets" value="cat"> Cat</label>
  <label><input type="checkbox" name="pets" value="bird"> Bird</label>

  <p id="pets-err" class="visually-hidden">You must select at least one.</p>
</fieldset>

Notes

  • Do not replace native checkboxes with div + ARIA unless necessary. If you must, use role="checkbox", manage tabindex, Space toggling, and aria-checked="true|false|mixed".

  • Form reset clears indeterminate to false; reapply in your code if a mixed state should remain after reset.

Indeterminate State

Use cases???

  • Supported: yes, via the HTMLInputElement.indeterminate property (JS-only). There is no indeterminate HTML attribute.

  • Semantics: a visual “mixed/partial” state. It does not affect form submission; only checked submits.

  • Interaction: on user click/Space, browsers automatically set indeterminate = false and then toggle checked. Re-set it in code if needed.

  • Styling: use the :indeterminate CSS pseudo-class.

  • A11y: announced as “mixed” by modern AT; keep a clear label and optional help text.

<label><input id="parent" type="checkbox"> Select all</label>
<div>
  <label><input class="child" type="checkbox"> A</label>
  <label><input class="child" type="checkbox"> B</label>
  <label><input class="child" type="checkbox"> C</label>
</div>

<script>
// parent controls children
const parent = document.getElementById('parent') as HTMLInputElement;
const kids = Array.from(document.querySelectorAll<HTMLInputElement>('.child'));

function syncParent() {
  const checked = kids.filter(k => k.checked).length;
  parent.checked = checked === kids.length && checked > 0;
  parent.indeterminate = checked > 0 && checked < kids.length;
}

parent.addEventListener('input', () => {
  kids.forEach(k => k.checked = parent.checked);
  parent.indeterminate = false;
});

kids.forEach(k => k.addEventListener('input', syncParent));
syncParent();
</script>

<style>
/* optional visual for mixed state */
input[type="checkbox"]:indeterminate { outline: 2px solid #f59e0b; }
</style>

Submitting a tri-state

  • Hidden fallback:

<input type="hidden" name="opt" value="none">
<input id="opt" type="checkbox" name="opt" value="all">
<script>
  const cb = document.getElementById('opt') as HTMLInputElement;
  // If you use a mixed state, set a separate hidden input to 'mixed'
</script>
  • Or use the formdata event to set exactly one value (none/mixed/all).

form.addEventListener('formdata', e => {
  let v = 'none';
  if (parent.indeterminate) v = 'mixed';
  else if (parent.checked) v = 'all';
  e.formData.set('opt', v);
});

Last updated