Basic Input
A labelled text input with helper text wired to it via aria-describedby.
6 frameworksBeginner
A bordered list of checkbox rows with a tri-state select-all and a live count in the header.
<!--
A bordered list of rows, each row a full-width <label> so the whole strip is
the target, not the 20px box. The header carries a select-all which is itself a
real checkbox - and it goes mixed when the selection is partial, because a
select-all that shows "unchecked" while three of five rows are ticked is lying.
The <fieldset>/<legend> pair is what gives the group a name; without it a
screen reader reads five unrelated checkboxes with no idea what they belong to.
-->
<fieldset class="list-group">
<legend class="list-group__legend">Sync these repositories</legend>
<div class="list-group__box">
<label class="list-group__row list-group__row--head" for="repos-all">
<input class="list-group__input list-group__input--all" type="checkbox" id="repos-all" />
<span class="list-group__title">Select all</span>
<span class="list-group__count" data-count>0 of 3 selected</span>
</label>
<label class="list-group__row" for="repo-web">
<input class="list-group__input list-group__input--item" type="checkbox" id="repo-web" name="repos" value="web" checked />
<span class="list-group__body">
<span class="list-group__title">adysre/web</span>
<span class="list-group__meta">Updated 2 hours ago</span>
</span>
</label>
<label class="list-group__row" for="repo-api">
<input class="list-group__input list-group__input--item" type="checkbox" id="repo-api" name="repos" value="api" />
<span class="list-group__body">
<span class="list-group__title">adysre/api</span>
<span class="list-group__meta">Updated yesterday</span>
</span>
</label>
<label class="list-group__row" for="repo-worker">
<input class="list-group__input list-group__input--item" type="checkbox" id="repo-worker" name="repos" value="worker" />
<span class="list-group__body">
<span class="list-group__title">adysre/worker</span>
<span class="list-group__meta">Updated last week</span>
</span>
</label>
</div>
</fieldset>
<script>
document.querySelectorAll('.list-group').forEach(function (root) {
var all = root.querySelector('.list-group__input--all');
var items = Array.prototype.slice.call(root.querySelectorAll('.list-group__input--item'));
var count = root.querySelector('[data-count]');
function sync() {
var checked = items.filter(function (item) { return item.checked; }).length;
var every = checked === items.length;
all.checked = every;
// DOM property - it has no HTML attribute, so this line is the only way
// the dash ever appears.
all.indeterminate = !every && checked > 0;
all.setAttribute('aria-checked', all.indeterminate ? 'mixed' : String(every));
count.textContent = checked + ' of ' + items.length + ' selected';
}
all.addEventListener('change', function () {
items.forEach(function (item) { item.checked = all.checked; });
sync();
});
items.forEach(function (item) { item.addEventListener('change', sync); });
sync();
});
</script>| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Prevents interaction and dims the control. |
className | string | - | Additional classes merged onto the root element. |
The select-all is tri-state on purpose. A select-all that shows plain "unchecked" while three of five rows are ticked is misreporting the selection, so it takes the same `indeterminate` DOM property as the tree component. Each row is a full-width `<label>`, which is what makes the whole strip the target rather than the 20px box - keep the `py-3` if you shrink the text. The card is a surface of its own, so dark mode moves the shell, the hairlines and the header tint together; changing only the border leaves a white card punched into a dark page.