Checkbox
Checkbox represents a binary or tri-state choice, such as accepting terms or toggling a setting. Grouped with others under a shared name, it becomes a way to pick any number of options from a list. It's not a wrapper around a native input. The whole element behaves as the control itself, so its accessible name, checked state, and validation all come from its own properties.
Dependencies
When to use
Reach for a checkbox whenever someone can pick more than one option at once, or toggle a single thing on and off, such as accepting terms, enabling a setting, or selecting several items from a list. A card-styled variant presents the same choice as a bigger, more visual tile instead of a plain inline box (see the Examples tab). If exactly one option should be true at a time, or a toggle should take effect immediately instead of waiting on a form submission, see Alternatives below for a better fit.
Install & usage
Pick a framework in the toolbar above and these snippets adapt.
npm install @eunomia/elements
import "@eunomia/elements/checkbox.js";
<eun-checkbox name="terms" value="accepted">
Accept the terms and conditions
</eun-checkbox>
Importing the file registers <eun-checkbox> as a custom element, with no
further setup needed. It works with any framework, or none, since it's a
standard web component.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/checkbox.js";
</script>
<eun-checkbox name="terms" value="accepted">
Accept the terms and conditions
</eun-checkbox>
npm install @eunomia/elements
import "@eunomia/elements/checkbox.js";
function TermsField() {
return (
<eun-checkbox name="terms" value="accepted">
Accept the terms and conditions
</eun-checkbox>
);
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/checkbox.js";
export function TermsField() {
return (
<eun-checkbox name="terms" value="accepted">
Accept the terms and conditions
</eun-checkbox>
);
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/checkbox.js";
</script>
<template>
<eun-checkbox name="terms" value="accepted">
Accept the terms and conditions
</eun-checkbox>
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/checkbox.js";
@Component({
selector: "app-terms-field",
template: `
<eun-checkbox name="terms" value="accepted">
Accept the terms and conditions
</eun-checkbox>
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class TermsFieldComponent {}
Grouping several checkboxes under one name? Import
@eunomia/elements/checkbox-list.js too. Its properties are in the API
tab, right below eun-checkbox's own, and the "Grouped list" section of
the Examples tab shows it customized. eun-checkbox-list doesn't have a
page of its own since it's really just eun-checkbox's own group
behavior, not a separate component to learn. It groups eun-check-card
children exactly the same way.
import "@eunomia/elements/checkbox.js";
import "@eunomia/elements/checkbox-list.js";
<eun-checkbox-list name="channels" label="Contact preferences">
<eun-checkbox value="email">Email</eun-checkbox>
<eun-checkbox value="sms">SMS</eun-checkbox>
</eun-checkbox-list>
Reaching for the card variant? Import @eunomia/elements/check-card.js
instead of (or alongside) checkbox.js. It's a separate entry point, not
bundled into eun-checkbox by default. Since eun-check-card extends
eun-checkbox, importing check-card.js alone already registers both
elements, so a separate checkbox.js import isn't required just to use the
card variant.
import "@eunomia/elements/check-card.js";
<eun-check-card value="wifi" icon="wifi">Wi-Fi</eun-check-card>
@eunomia/elements/checkbox.js to one shared module no
matter how many files import it, so this doesn't apply there. It only
matters if you self-host separately pre-bundled files
for each component (the way this documentation site itself loads its
live previews) and reference both with plain
<script type="module" src="…"> tags. Load
check-card.js before checkbox.js. Whichever
loads first "wins" the shared eun-checkbox custom-element
registration, and the other logs a harmless
"already been used" console error and stops there, but if
checkbox.js loads first, that error happens
inside check-card.js's own bundle (which also
needs to define eun-checkbox to extend it), aborting the
rest of that script before it reaches its own
eun-check-card registration, leaving
<eun-check-card> undefined and rendered as plain,
unstyled text.
<!-- Correct order for plain <script> tags, no bundler: -->
<script type="module" src="check-card.js"></script>
<script type="module" src="checkbox.js"></script>
Alternatives
eun-radioeun-switchGuidance
- Always set
label(or slot text content): see the Accessibility tab - Use a single checkbox for a standalone binary choice (accept/enable), and a
eun-checkbox-listfor "select any of N" from a list - Reserve
indeterminatefor a checkbox that summarizes a partially-selected group (see the Examples tab) - Pair a
requiredgroup with a visible note at the top of the form, not just the error message alone
- Using checkboxes for a single mutually-exclusive choice: see Alternatives above
- Using a checkbox list for more than ~5-6 options: a multi-select would scale better
- Hiding the
hintin a tooltip instead of rendering it under the field - Overriding colors with inline styles instead of the
--checkbox-*CSS variables
Live testing
Properties
Checkbox <eun-checkbox>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| default-checked | boolean | — | The checked state applied when the checkbox connects, and restored on form reset |
| hide-error | boolean | false | Whether the error message is hidden |
| default-value | string | — | The default value applied when the checkbox connects, and restored on form reset |
| indeterminate | boolean | false | Whether the checkbox value is indeterminate |
| label | string | — | The label to display when not using the default slot |
Import the exact TypeScript type behind any property above, see
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| checked | boolean | false | Whether the checkbox is checked |
| defaultChecked | boolean | — | The checked state applied when the checkbox connects, and restored on form reset |
| disabled | boolean | false | Whether the checkbox is disabled |
| readonly | boolean | false | Whether the checkbox is read only |
| hideError | boolean | false | Whether the error message is hidden |
| required | boolean | false | Whether a selection is required |
| name | string | — | The name of the checkbox field |
| value | string | — | The value submitted with the checkbox |
| hint | string | — | A short message displayed under the checkbox |
| validators | Array<Validators<boolean>> | — | A list of validation rules applied to the checked state |
Slots
| Name | Description |
|---|---|
| (default) | The checkbox label |
Events
| Name | Type | Description |
|---|---|---|
| click | PointerEvent | Fired when the checkbox is clicked, including while indeterminate |
| eunchange | ChangeEvent | Fired when the checkbox's checked state changes |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --checkbox-background | Sets the background color while not checked |
| --checkbox-background-checked | Sets the background color while checked |
| --checkbox-background-hover | Sets the background color on hover |
| --checkbox-background-pressed | Sets the background color while pressed |
| --checkbox-icon-color | Sets the color of the check icon |
| --checkbox-error-color | Sets the color of the error message |
| --checkbox-background-error | Sets the background color while checked and invalid, falling back to checkbox-error-color when unset |
| --checkbox-hint-color | Sets the color of the hint message |
| --checkbox-size | Sets the size of the checkbox |
| --checkbox-border-color | Sets the border color of the checkbox |
| --checkbox-border-radius | Sets the corner radius of the box |
| --checkbox-focus-outline-color | Sets the color of the focus outline used for keyboard navigation |
Into a list
eun-checkbox-list groups eun-checkbox children into a single "select N of
M" field: a fieldset/legend wrapper that propagates name/disabled/
readonly down to every child and exposes the group's selection as a plain
value: string[], with its own eunchange event and validators. It
doesn't have a documentation page of its own. This is its whole API. See
the Examples tab ("Grouped list") for customizing it: inline layout, group
validation, disabled/readonly, and a full indeterminate "select all" pattern.
Checkbox list <eun-checkbox-list>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| default-value | string | — | The default selected values, as a comma-separated string |
| hide-error | boolean | false | Whether the errors are hidden |
| inline | boolean | false | Whether the list's elements are displayed in a row |
Import the exact TypeScript type behind any property above, see
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| fields | EunomiaCheckbox[] | — | Getter allowing to recover the direct `eun-checkbox` (or `eun-check-card`) children of the list. |
| value | string[] | — | Setter allowing to check every child whose value is in the given list. |
| label | string | — | The title label of the list |
| instructions | string | — | Instructions for the user about the list selection |
| name | string | — | The field name, propagated to every child checkbox |
| disabled | boolean | false | Whether the list is disabled |
| readonly | boolean | false | Whether the list is read only |
| required | boolean | false | Whether at least one selection is required |
| defaultValue | string[] | — | The default selected values |
| hideError | boolean | false | Whether the errors are hidden |
| validators | Array<Validators<string[]>> | — | A list of validation rules applied to the group's selected values |
| description | — | Gets the current description or validation errors. Returns undefined if errors are hidden. | |
| form | — | Reference to the native form element this control belongs to. | |
| type | — | Returns the local tag name as the type of the field. |
Slots
| Name | Description |
|---|---|
| (default) | The list content. Only direct checkbox or check card children participate |
| label | The title label of the list |
| instructions | Instructions for the user about the list selection |
Events
| Name | Type | Description |
|---|---|---|
| eunchange | ChangeEvent | Fired whenever the group's selection changes |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --checkbox-list-gap | Sets the gap between the labels and the list |
| --checkbox-list-options-gap | Sets the gap between the checkboxes in the list |
| --checkbox-list-error-color | Sets the color of the list's error state |
Card variant
eun-check-card extends eun-checkbox directly, so every property above
applies unchanged: only icon is new, and styles/render() are
overridden for the card look. See the --check-card-* custom properties
below for the card-specific visual variables (--checkbox-* itself doesn't
apply, since the card shares no CSS selectors with the plain box/label
markup).
Check card <eun-check-card>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| default-checked | boolean | — | The checked state applied when the card connects, and restored on form reset |
| hide-error | boolean | false | Whether the error message is hidden |
| default-value | string | — | The default value applied when the card connects, and restored on form reset |
| icon | EunomiaIconName | — | An optional leading icon rendered inside the card |
| indeterminate | boolean | false | Whether the checkbox value is indeterminate |
| label | string | — | The label to display when not using the default slot |
Import the exact TypeScript type behind any property above, see
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| checked | boolean | false | Whether the card is checked |
| defaultChecked | boolean | — | The checked state applied when the card connects, and restored on form reset |
| disabled | boolean | false | Whether the card is disabled |
| readonly | boolean | false | Whether the card is read only |
| hideError | boolean | false | Whether the error message is hidden |
| required | boolean | false | Whether a selection is required |
| name | string | — | The name of the field |
| value | string | — | The value submitted with the card |
| hint | string | — | A short message displayed under the card |
| validators | Array<Validators<boolean>> | — | A list of validation rules applied to the checked state |
Slots
| Name | Description |
|---|---|
| (default) | The card label |
| icon | Replaces the leading icon entirely with any content, not just an icon element. Falls back to icon when empty |
Events
| Name | Type | Description |
|---|---|---|
| click | PointerEvent | Fired when the card is clicked, including while indeterminate |
| eunchange | ChangeEvent | Fired when the card's checked state changes |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --check-card-background | Sets the background color while not checked |
| --check-card-background-hover | Sets the background color on hover |
| --check-card-background-pressed | Sets the background color while pressed |
| --check-card-background-checked | Sets the background color while checked |
| --check-card-background-error | Sets the background color while checked and invalid, falling back to check-card-error-color when unset |
| --check-card-border-color | Sets the border color while not checked |
| --check-card-border-color-hover | Sets the border color on hover |
| --check-card-border-color-checked | Sets the border color while checked |
| --check-card-border-width | Sets the width of the border |
| --check-card-border-radius | Sets the corner radius of the card |
| --check-card-padding | Sets the padding inside the card |
| --check-card-min-width | Sets the minimum width of the card |
| --check-card-icon-color | Sets the color of the leading icon while not checked |
| --check-card-icon-color-checked | Sets the color of the leading icon while checked |
| --check-card-error-color | Sets the color of the error message |
| --check-card-hint-color | Sets the color of the hint message |
| --check-card-focus-outline-color | Sets the color of the focus outline used for keyboard navigation |
| --checkbox-background | Sets the background color while not checked |
| --checkbox-background-checked | Sets the background color while checked |
| --checkbox-background-hover | Sets the background color on hover |
| --checkbox-background-pressed | Sets the background color while pressed |
| --checkbox-icon-color | Sets the color of the check icon |
| --checkbox-error-color | Sets the color of the error message |
| --checkbox-background-error | Sets the background color while checked and invalid, falling back to checkbox-error-color when unset |
| --checkbox-hint-color | Sets the color of the hint message |
| --checkbox-size | Sets the size of the checkbox |
| --checkbox-border-color | Sets the border color of the checkbox |
| --checkbox-border-radius | Sets the corner radius of the box |
| --checkbox-focus-outline-color | Sets the color of the focus outline used for keyboard navigation |
Basic
<eun-checkbox name="terms" value="accepted">
Accept the terms and conditions
</eun-checkbox>
States
Disabled
Disabled checkboxes remain visible but can't be toggled, and are excluded
from the tab order and from form submission. A disabled checkbox always
renders as unchecked, regardless of its checked property.
<eun-checkbox disabled>Disabled</eun-checkbox>
Readonly
A readonly checkbox looks active, stays focusable, and its value still
submits, but it can't be toggled by the user. Unlike disabled, it still
communicates its current state instead of looking inert.
<eun-checkbox readonly>Readonly, unchecked</eun-checkbox>
<eun-checkbox readonly checked>Readonly, checked</eun-checkbox>
Indeterminate
A visual "partially selected" state, useful to summarize a group where only
some children are checked. Setting it doesn't change checked, and pressing
Space (or clicking) while indeterminate doesn't toggle it either. It only
forwards a plain click event and leaves the state alone, so the consumer
decides what "next" means for the group it summarizes (see the Accessibility
tab). Here's the full pattern: a "select all" checkbox kept in sync with a
eun-checkbox-list:
<eun-checkbox id="select-all">Select all</eun-checkbox>
<eun-checkbox-list id="fruits" name="fruits">
<eun-checkbox value="apple">Apple</eun-checkbox>
<eun-checkbox value="banana" checked>Banana</eun-checkbox>
<eun-checkbox value="cherry">Cherry</eun-checkbox>
</eun-checkbox-list>
const selectAll = document.querySelector("#select-all");
const list = document.querySelector("#fruits");
function syncSelectAll() {
const checkedCount = list.value.length;
const total = list.fields.length;
selectAll.checked = checkedCount === total;
selectAll.indeterminate = checkedCount > 0 && checkedCount < total;
}
// Setting `list.value` doesn't fire the list's own `eunchange` on its own
// (same as a native `<input>.value = ...>` firing no `input`/`change`
// event) — call `syncSelectAll` explicitly right after, rather than
// relying on the list to announce a change it never actually dispatches.
// The plain toggle case : `checked` already reflects the new state by the
// time `eunchange` fires, so mirror it onto the list.
selectAll.addEventListener("eunchange", () => {
list.value = selectAll.checked ? list.fields.map((field) => field.value) : [];
list.updateComplete.then(syncSelectAll);
});
// The indeterminate case never changes `checked` (so `eunchange` never
// fires) — it forwards a `click` instead, specifically so this is
// reachable at all. See the Accessibility tab.
selectAll.addEventListener("click", () => {
if (selectAll.indeterminate) {
list.value = list.fields.map((field) => field.value);
list.updateComplete.then(syncSelectAll);
}
});
// The list's own `eunchange` still fires on every *user-driven* check/uncheck
// of an individual child, which is what keeps `selectAll` in sync the rest
// of the time.
list.addEventListener("eunchange", syncSelectAll);
syncSelectAll();
Error
<eun-checkbox required hint="You must accept the terms to continue">
Accept the terms and conditions
</eun-checkbox>
required alone is enough: it's checked against checked, not against the
static value attribute (see the Accessibility tab). Add validators on top
for anything beyond "must be checked":
document.querySelector("eun-checkbox").validators = [
{
isValid: (checked: boolean) => checked,
message: "You must accept the terms to continue",
},
];
A checked box that's still invalid (e.g. one of several checked options in
a group that, together, don't yet satisfy a validators rule, see "Group
validation" below) fills with the error color instead of just outlining it,
so it reads as "part of the problem" rather than blending in as if
everything were fine. Override that fill with --checkbox-background-error
if the plain error color isn't distinct enough against a checked box's own
background.
Custom box
Override the --checkbox-* CSS variables, listed in full in the API tab.
<eun-checkbox class="custom-checkbox">Custom checkbox</eun-checkbox>
.custom-checkbox {
--checkbox-background: #fdf2f8;
--checkbox-background-hover: #fce7f3;
--checkbox-background-pressed: #fbcfe8;
--checkbox-background-checked: #be185d;
--checkbox-icon-color: #fff;
}
Grouped list
eun-checkbox-list wraps a set of eun-checkbox children in a fieldset /
legend so the group reads as one unit to assistive technology. Each child
checkbox still submits its own value under the shared name independently
when checked, exactly like a native group of <input type="checkbox">
sharing a name, so this component never duplicates what its children
already do on their own. It only adds grouping semantics and a convenience
API on top. Its properties are documented in the API tab, right below
eun-checkbox's own. The examples below are about customizing it.
Import both modules to use it:
import "@eunomia/elements/checkbox.js";
import "@eunomia/elements/checkbox-list.js";
<eun-checkbox-list
name="channels"
label="Contact preferences"
instructions="Select every channel you want to be notified on"
>
<eun-checkbox value="email">Email</eun-checkbox>
<eun-checkbox value="phone">Phone</eun-checkbox>
<eun-checkbox value="sms">SMS</eun-checkbox>
</eun-checkbox-list>
Read the selection back as a plain array, and listen for eunchange on the
list rather than on every child individually:
const list = document.querySelector("eun-checkbox-list");
list.addEventListener("eunchange", () => console.log(list.value)); // e.g. ["email", "sms"]
Inline layout
<eun-checkbox-list name="channels-inline" label="Contact preferences" inline>
<eun-checkbox value="email">Email</eun-checkbox>
<eun-checkbox value="phone">Phone</eun-checkbox>
<eun-checkbox value="sms">SMS</eun-checkbox>
</eun-checkbox-list>
Group validation
required on the list means "at least one option checked": its value
getter is a real array reflecting the current selection, so the built-in
check already works with no extra wiring. Layer validators on top for
anything more specific:
const list = document.querySelector("eun-checkbox-list");
list.validators = [
{
isValid: (values: string[]) => values.length >= 2,
message: "Select at least two channels",
},
];
list.checkValidity(list.value);
The list propagates its own valid down to every child, so all of them
render invalid together, but only a checked one also picks up the
--checkbox-background-error fill (see the Error state above), visually
telling apart "checked but not enough" from "not checked yet":
Disabled and readonly
Setting disabled or readonly on the list propagates it to every child
checkbox, no need to repeat it on each one.
<eun-checkbox-list
name="channels-disabled"
label="Contact preferences"
disabled
>
<eun-checkbox value="email" checked>Email</eun-checkbox>
<eun-checkbox value="phone">Phone</eun-checkbox>
</eun-checkbox-list>
Card display
Import both modules to use it:
import "@eunomia/elements/check-card.js";
import "@eunomia/elements/checkbox-list.js";
Basic, with icons
<eun-checkbox-list name="addons" label="Add-ons" inline>
<eun-check-card value="wifi" icon="wifi">Wi-Fi</eun-check-card>
<eun-check-card value="shipping" icon="local_shipping"
>Shipping</eun-check-card
>
<eun-check-card value="insurance" icon="security">Insurance</eun-check-card>
</eun-checkbox-list>
Without an icon
icon is entirely optional: the label alone works too.
<eun-check-card name="newsletter" value="subscribed">
Subscribe to the newsletter
</eun-check-card>
Custom icon
Need something other than a eun-icon, or a different icon library
entirely? Slot your own content into icon instead. It replaces the
prop-driven icon entirely.
<eun-check-card value="wifi">
<svg
slot="icon"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M5 13a10 10 0 0 1 14 0M8.5 16.5a5 5 0 0 1 7 0M12 20h.01"></path>
</svg>
Wi-Fi
</eun-check-card>
Disabled, readonly, and error
Three independent fields side by side for comparison, not a group. Each
keeps its own state directly, so they're plain siblings here rather than
wrapped in a eun-checkbox-list. The list always propagates its own
disabled/readonly down to every child on connect, which would overwrite
each card's individually-set state with the list's shared one.
<eun-check-card value="wifi" icon="wifi" disabled>Disabled</eun-check-card>
<eun-check-card value="wifi" icon="wifi" readonly checked
>Readonly</eun-check-card
>
<eun-check-card value="wifi" icon="wifi" required hint="Required"
>Required</eun-check-card
>
Grouped in a list
eun-checkbox-list recognizes eun-check-card children exactly like plain
eun-checkbox: same name propagation, same group value: string[], same
disabled/readonly/validators propagation (see "Grouped list" above).
<eun-checkbox-list name="addons" label="Choose your add-ons" inline>
<eun-check-card value="wifi" icon="wifi">Wi-Fi</eun-check-card>
<eun-check-card value="shipping" icon="local_shipping">
Shipping
</eun-check-card>
<eun-check-card value="insurance" icon="security">Insurance</eun-check-card>
</eun-checkbox-list>
Custom card
Override the --check-card-* CSS variables, listed in full in the API tab.
<eun-check-card class="custom-check-card" icon="star" checked>
Custom card
</eun-check-card>
.custom-check-card {
--check-card-border-color-checked: #be185d;
--check-card-background-checked: #fdf2f8;
--check-card-icon-color-checked: #be185d;
}
This audit covers eun-checkbox standalone and grouped in
eun-checkbox-list, since grouping changes what's on screen and what
accessibility tree gets built around it.
Keyboard interactions
| Key | Action |
|---|---|
Tab / Shift+Tab |
Moves focus in/out of the checkbox (single tab stop) |
Space |
Toggles checked, or, while indeterminate, forwards a click event instead of toggling (see below) |
Inside a eun-checkbox-list, Tab moves between the checkboxes themselves.
The group is a single accessible unit (fieldset/legend) but not a single
tab stop. Each checkbox keeps its own place in the tab order, the same way
native <input type="checkbox"> sharing a name behaves.
Aria attributes
eun-checkbox doesn't wrap a native <input>. The host element itself is
the control, so every ARIA state lives directly on it:
role="checkbox", set on connect if not already present on the hostaria-checked="true"\|"false"\|"mixed": reflectschecked, or"mixed"whileindeterminateand uncheckedaria-disabled/aria-readonly: kept in sync withdisabled/readonlyaria-required: kept in sync withrequiredaria-invalid: kept in sync withvalid, oncedisabledorhideErrorare also accounted foraria-description: set fromhint/error content while present (via the inheriteddescription), removed otherwisearia-label: set from thelabelproperty when it's used (see Label accessibility below)
eun-checkbox-list wraps its children in role="group" (on the options
container) inside a real <fieldset>/<legend>, with aria-labelledby
pointing at the legend and aria-describedby at its error/hint message:
the standard pattern for a labeled group of checkboxes, matching a native
<fieldset> around a group of <input type="checkbox">.
Label accessibility
role="checkbox" requires an author-supplied accessible name. Unlike
role="button", for example, ARIA does not allow deriving one from content
for this role. Two ways to provide it, both supported:
- Set the
labelproperty:eun-checkboxsetsaria-labelon the host directly from it, an explicit, guaranteed accessible name. - Slot text content instead (
<eun-checkbox>Accept the terms</eun-checkbox>, used throughout this page). No explicitaria-labelis set in that case, so the name comes from the flattened shadow-tree content instead: the slotted text is what ends up read as the accessible name in every browser tested, but this relies on content-based name computation rather than an explicit author-supplied one.
Always provide one or the other. An unlabelled checkbox is exactly as disruptive for screen-reader users as an unlabelled text field.
Inside a eun-checkbox-list, always set the list's own label (or slot one)
too: it names the group, distinct from each checkbox's own label naming
its individual option.
Disabled vs. readonly
disabled removes the checkbox from the tab order and excludes its value
from form submission entirely: it also forces the checkbox to render
unchecked regardless of its actual checked value, since a disabled control
communicating a hidden checked state serves nobody. readonly keeps it
focusable and its value submitted, and additionally prevents toggling,
communicating "you can't change this right now", not "this doesn't apply".
A disabled checkbox may cause usability and accessibility issues for
people with disabilities relying on that state to still be inspectable.
Prefer readonly whenever the value still needs to stay perceivable.
Indeterminate state
indeterminate only changes what's rendered (aria-checked="mixed", a dash
icon): it never changes checked on its own. Pressing Space while
indeterminate forwards a plain click event instead of toggling, since a
tri-state summary checkbox (e.g. "select all" over a partially-checked list)
has no single obvious "next" boolean state, so the consumer is expected to
listen for that click and decide what selecting it should do to the group
it summarizes (typically: check everything). This keeps mouse and keyboard
parity. A real mouse click already bubbles a native click event regardless
of indeterminate, so the keyboard path forwards an equivalent event rather
than silently doing nothing on Space.
A plain toggle (not indeterminate) stops its own native click from bubbling
past the checkbox once handled, so a listener attached directly on the
element only reliably sees it while indeterminate. For the regular
checked/unchecked case, listen for eunchange instead, which always fires.
See the full "select all" example in the Examples tab, which relies on
exactly that split.
Required field
required means "must be checked", validated against the checkbox's actual
checked state, not against its value attribute, which is typically a
fixed string (e.g. value="accepted") present whether or not the box is
checked, and would otherwise never look "empty" to a generic required check.
On a eun-checkbox-list, required means "at least one checked", validated
against the group's real selection.
Add a visible note near the top of the form in addition to relying on the
error message alone, and prefer hint (or a group-level validators message
on eun-checkbox-list) to explain why a selection is required, not just
that it is.
Screen reader restitution
By default, screen readers read out the name, checked/mixed state, and required state. The order may vary depending on the screen reader and its configuration. The disabled state is rendered differently depending on the screen reader:
- VoiceOver (macOS/iOS): grayed out
- NVDA and JAWS: unavailable
- Narrator and TalkBack: disabled
Card variant (eun-check-card)
eun-check-card extends eun-checkbox directly rather than reimplementing
it, so every ARIA attribute, keyboard interaction, and label requirement
documented above carries over completely unchanged: role="checkbox",
aria-checked/aria-disabled/aria-readonly/aria-required/aria-invalid,
Space to toggle, the same label/slot accessible-name requirement. Two
differences worth calling out:
- The entire card is the click target, not just the small checked indicator in its corner, a strictly larger hit area than a plain checkbox's own box, which helps meet WCAG 2.5.5 Target Size.
- The optional leading
iconis purely decorative (eun-iconrenders its SVG witharia-hidden="true", see the Icon component's own documentation). It never carries meaning on its own, only reinforcing the text label already present. Never rely on the icon alone to convey what the option is.
Reference links