Checkbox Technical Notes
Submission Basics
A checked checkbox submits
name=VALUE
. If you don’t setvalue
, browsers sendon
.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:
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>
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>
Handle on the server: treat missing key as false.
A11y checklist
Provide a real
<label>
(wrap orfor
). 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 extrarole
).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 witharia-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, userole="checkbox"
, managetabindex
, Space toggling, andaria-checked="true|false|mixed"
.Form reset clears
indeterminate
tofalse
; reapply in your code if a mixed state should remain after reset.
Indeterminate State
Use cases???
CMS DATA-BINDING Not suitable for CMS Switch field binding. Would need to use a text field likely.
Supported: yes, via the
HTMLInputElement.indeterminate
property (JS-only). There is noindeterminate
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 togglechecked
. 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