Skip to main content
Being Idea Innovations
How to Check Which Checkboxes and Radio Buttons Are Selected in JavaScript
Back to Blog

How to Check Which Checkboxes and Radio Buttons Are Selected in JavaScript

17 November 20185 min readJavascript
Share:

Reading which checkboxes or radio buttons a user has selected is a routine task with several sharp edges — particularly once you need groups that exclude one another, or a "select all" that reflects a partial selection.

Everything here is plain JavaScript. The jQuery approaches most tutorials show rely on .live(), which was removed in jQuery 1.9 back in 2013 — code using it does nothing at all on any modern version.

Checking a single input

const terms = document.getElementById('terms');

if (terms.checked) {
  // ticked
}

Use the .checked property, not the attribute. getAttribute('checked') returns the initial markup state and never changes as the user clicks. This trips people up constantly.

input.checked;                    // true / false — current state ✅
input.getAttribute('checked');    // reflects the HTML, not the user ❌

Working with a group

<fieldset>
  <legend>Extras</legend>
  <label><input type="checkbox" name="extras" value="gift-wrap" data-price="350" /> Gift wrap</label>
  <label><input type="checkbox" name="extras" value="express" data-price="900" /> Express delivery</label>
  <label><input type="checkbox" name="extras" value="insurance" data-price="250" /> Insurance</label>
</fieldset>
const all = document.querySelectorAll('input[name="extras"]');

// The :checked pseudo-class does the filtering for you.
const checked = document.querySelectorAll('input[name="extras"]:checked');

const anyChecked  = checked.length > 0;
const allChecked  = checked.length === all.length;
const noneChecked = checked.length === 0;

// Values of everything ticked
const values = [...checked].map(input => input.value);
// ["gift-wrap", "express"]

querySelectorAll returns a NodeList, not an array. It supports forEach, but for map or filter you need to spread it first, as above.

Using the form itself

If the inputs are inside a <form>, FormData collects them for you:

const data = new FormData(document.getElementById('order-form'));
const extras = data.getAll('extras');   // ["gift-wrap", "express"]

Note getAll(), not get()get() returns only the first value, which is a quiet source of bugs with checkbox groups.

Radio buttons

A radio group has at most one selection, so you want the element rather than a list:

const selected = document.querySelector('input[name="plan"]:checked');

// Nothing chosen yet returns null — always guard.
const plan = selected ? selected.value : null;

Radios are grouped by their shared name. If two "groups" on a page share a name, they behave as one group and selecting in either clears the other — a common cause of confusing behaviour on forms with repeated sections.

Summing selected values

const total = [...document.querySelectorAll('input[name="extras"]:checked')]
  .reduce((sum, input) => sum + Number(input.dataset.price), 0);

Keep money in integers. The prices above are in paise, not rupees. Floating point cannot represent most decimal fractions exactly, so summing 4.27 + 1.39 can yield 5.659999999999999. Work in the smallest unit and divide only when displaying:

const display = (total / 100).toFixed(2);

Event delegation — the replacement for .live()

.live() existed to handle events on elements added after page load. The native equivalent is a listener on a stable ancestor:

document.getElementById('options').addEventListener('change', (e) => {
  const input = e.target;
  if (!input.matches('input[type="checkbox"], input[type="radio"]')) return;

  updateSummary();
});

One listener covers every input inside #options, including ones added later. Use the change event rather than clickchange also fires for keyboard selection, where click alone can miss it.

Select-all with an indeterminate state

A "select all" checkbox should show a partial state when only some children are ticked:

const master = document.getElementById('select-all');
const items  = document.querySelectorAll('input[name="items"]');

function syncMaster() {
  const checked = document.querySelectorAll('input[name="items"]:checked').length;

  master.checked = checked === items.length;
  // indeterminate is a property only — there is no HTML attribute for it.
  master.indeterminate = checked > 0 && checked < items.length;
}

master.addEventListener('change', () => {
  items.forEach(item => { item.checked = master.checked; });
  master.indeterminate = false;
});

items.forEach(item => item.addEventListener('change', syncMaster));
syncMaster();

indeterminate is purely visual — the input still submits as either checked or unchecked. It cannot be set in markup, only from JavaScript.

Mutually exclusive groups

A common requirement: a set of checkboxes and a set of radios where choosing from one clears the other — "either apply these discounts, or apply this voucher, not both".

const discountBoxes = document.querySelectorAll('input[name="discounts"]');
const voucherRadios = document.querySelectorAll('input[name="voucher"]');

function clear(inputs) {
  inputs.forEach(input => { input.checked = false; });
}

discountBoxes.forEach(box =>
  box.addEventListener('change', () => {
    if (box.checked) clear(voucherRadios);
    updateSummary();
  })
);

voucherRadios.forEach(radio =>
  radio.addEventListener('change', () => {
    if (radio.checked) clear(discountBoxes);
    updateSummary();
  })
);

Two things to get right. Announce the change — silently unticking a user's earlier choice is confusing, so update a live region explaining what happened. And enforce the rule on the server too; anyone can submit both groups by bypassing your JavaScript entirely, which on a discount form is a direct financial exploit.

Storing JSON in a data attribute

The original version of this article did this:

<!-- BROKEN: the inner quotes terminate the attribute -->
<input data-itemcode="{"PromoId":1317,"Amount":1.39}" />

That does not parse. The HTML parser sees the attribute end at the second ", so PromoId becomes a stray attribute name and the JSON is lost. Encode the quotes:

<input data-itemcode='{"promoId":1317,"amount":139}' />

<!-- or, if you must use double quotes around the attribute -->
<input data-itemcode="{&quot;promoId&quot;:1317,&quot;amount&quot;:139}" />

In PHP, let the escaping function handle it:

<input data-itemcode="<?= htmlspecialchars(json_encode($item), ENT_QUOTES, 'UTF-8') ?>" />

Then read it back:

const item = JSON.parse(input.dataset.itemcode);

Simpler still: for a couple of scalar values, use separate attributes — data-promo-id and data-amount — and skip JSON entirely.

readonly does not work on checkboxes

Another bug in the original markup: readonly="" on checkbox and radio inputs. The readonly attribute has no effect on checkboxes or radios — it applies to text inputs only. Users can still toggle them.

Use disabled instead, but note the trade-off: disabled inputs are excluded from form submission and skipped in the tab order. To show a value that is fixed but must still submit, use a readonly display element plus a hidden input:

<input type="checkbox" checked disabled />
<input type="hidden" name="extras" value="gift-wrap" />

Accessibility

  • Wrap groups in <fieldset> with a <legend> so screen readers announce what the choice is about.
  • Associate a real <label> with every input — nesting the input inside the label is the simplest way.
  • Announce dynamic totals in an aria-live="polite" region.
  • Never rely on colour alone for the checked state.

Quick reference

TaskCode
Is one checked?input.checked
All checked in a groupdocument.querySelectorAll('[name="x"]:checked')
How many checked…:checked').length
Selected radiodocument.querySelector('[name="x"]:checked')
All values[...checked].map(i => i.value)
From a formnew FormData(form).getAll('x')
Partial stateinput.indeterminate = true
Share:

Written by

Amit Verma

Founder / Senior Software Engineer

Amit leads engineering at Being Idea. With 15+ years building scalable software products across global markets, he drives architecture decisions and engineering culture across every engagement.

More articles by Amit

Want to talk tech?

We ship software that scales. Let's work together.

No long-term contracts
Senior engineers only
US · AU · NZ timezone coverage
14-day trial on retainers