Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

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

eun-icon eun-label · for eun-checkbox-list, if it has a label
Overview API Examples Accessibility

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.

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>
Using a bundler (Vite, webpack, Rollup, …) or an import map resolves @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

You want .. Prefers Exactly one option to be true at a time eun-radio A toggle that takes effect immediately, not part of a form submitted later eun-switch

Guidance

  • 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-list for "select any of N" from a list
  • Reserve indeterminate for a checkbox that summarizes a partially-selected group (see the Examples tab)
  • Pair a required group 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 hint in 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

NameTypeDefaultDescription
default-checkedbooleanThe checked state applied when the checkbox connects, and restored on form reset
hide-errorbooleanfalseWhether the error message is hidden
default-valuestringThe default value applied when the checkbox connects, and restored on form reset
indeterminatebooleanfalseWhether the checkbox value is indeterminate
labelstringThe label to display when not using the default slot

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
checkedbooleanfalseWhether the checkbox is checked
defaultCheckedbooleanThe checked state applied when the checkbox connects, and restored on form reset
disabledbooleanfalseWhether the checkbox is disabled
readonlybooleanfalseWhether the checkbox is read only
hideErrorbooleanfalseWhether the error message is hidden
requiredbooleanfalseWhether a selection is required
namestringThe name of the checkbox field
valuestringThe value submitted with the checkbox
hintstringA short message displayed under the checkbox
validatorsArray<Validators<boolean>>A list of validation rules applied to the checked state

Slots

NameDescription
(default)The checkbox label

Events

NameTypeDescription
clickPointerEventFired when the checkbox is clicked, including while indeterminate
eunchangeChangeEventFired when the checkbox's checked state changes

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--checkbox-backgroundSets the background color while not checked
--checkbox-background-checkedSets the background color while checked
--checkbox-background-hoverSets the background color on hover
--checkbox-background-pressedSets the background color while pressed
--checkbox-icon-colorSets the color of the check icon
--checkbox-error-colorSets the color of the error message
--checkbox-background-errorSets the background color while checked and invalid, falling back to checkbox-error-color when unset
--checkbox-hint-colorSets the color of the hint message
--checkbox-sizeSets the size of the checkbox
--checkbox-border-colorSets the border color of the checkbox
--checkbox-border-radiusSets the corner radius of the box
--checkbox-focus-outline-colorSets 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

NameTypeDefaultDescription
default-valuestringThe default selected values, as a comma-separated string
hide-errorbooleanfalseWhether the errors are hidden
inlinebooleanfalseWhether the list's elements are displayed in a row

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
fieldsEunomiaCheckbox[]Getter allowing to recover the direct `eun-checkbox` (or `eun-check-card`) children of the list.
valuestring[]Setter allowing to check every child whose value is in the given list.
labelstringThe title label of the list
instructionsstringInstructions for the user about the list selection
namestringThe field name, propagated to every child checkbox
disabledbooleanfalseWhether the list is disabled
readonlybooleanfalseWhether the list is read only
requiredbooleanfalseWhether at least one selection is required
defaultValuestring[]The default selected values
hideErrorbooleanfalseWhether the errors are hidden
validatorsArray<Validators<string[]>>A list of validation rules applied to the group's selected values
descriptionGets the current description or validation errors. Returns undefined if errors are hidden.
formReference to the native form element this control belongs to.
typeReturns the local tag name as the type of the field.

Slots

NameDescription
(default)The list content. Only direct checkbox or check card children participate
labelThe title label of the list
instructionsInstructions for the user about the list selection

Events

NameTypeDescription
eunchangeChangeEventFired whenever the group's selection changes

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--checkbox-list-gapSets the gap between the labels and the list
--checkbox-list-options-gapSets the gap between the checkboxes in the list
--checkbox-list-error-colorSets 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

NameTypeDefaultDescription
default-checkedbooleanThe checked state applied when the card connects, and restored on form reset
hide-errorbooleanfalseWhether the error message is hidden
default-valuestringThe default value applied when the card connects, and restored on form reset
iconEunomiaIconNameAn optional leading icon rendered inside the card
indeterminatebooleanfalseWhether the checkbox value is indeterminate
labelstringThe label to display when not using the default slot

Import the exact TypeScript type behind any property above, see Types.

Properties

JS-only — no matching HTML attribute, set these from a script or a template binding.

NameTypeDefaultDescription
checkedbooleanfalseWhether the card is checked
defaultCheckedbooleanThe checked state applied when the card connects, and restored on form reset
disabledbooleanfalseWhether the card is disabled
readonlybooleanfalseWhether the card is read only
hideErrorbooleanfalseWhether the error message is hidden
requiredbooleanfalseWhether a selection is required
namestringThe name of the field
valuestringThe value submitted with the card
hintstringA short message displayed under the card
validatorsArray<Validators<boolean>>A list of validation rules applied to the checked state

Slots

NameDescription
(default)The card label
iconReplaces the leading icon entirely with any content, not just an icon element. Falls back to icon when empty

Events

NameTypeDescription
clickPointerEventFired when the card is clicked, including while indeterminate
eunchangeChangeEventFired when the card's checked state changes

Every event above follows the same naming convention, covered in Events.

CSS custom properties

NameDescription
--check-card-backgroundSets the background color while not checked
--check-card-background-hoverSets the background color on hover
--check-card-background-pressedSets the background color while pressed
--check-card-background-checkedSets the background color while checked
--check-card-background-errorSets the background color while checked and invalid, falling back to check-card-error-color when unset
--check-card-border-colorSets the border color while not checked
--check-card-border-color-hoverSets the border color on hover
--check-card-border-color-checkedSets the border color while checked
--check-card-border-widthSets the width of the border
--check-card-border-radiusSets the corner radius of the card
--check-card-paddingSets the padding inside the card
--check-card-min-widthSets the minimum width of the card
--check-card-icon-colorSets the color of the leading icon while not checked
--check-card-icon-color-checkedSets the color of the leading icon while checked
--check-card-error-colorSets the color of the error message
--check-card-hint-colorSets the color of the hint message
--check-card-focus-outline-colorSets the color of the focus outline used for keyboard navigation
--checkbox-backgroundSets the background color while not checked
--checkbox-background-checkedSets the background color while checked
--checkbox-background-hoverSets the background color on hover
--checkbox-background-pressedSets the background color while pressed
--checkbox-icon-colorSets the color of the check icon
--checkbox-error-colorSets the color of the error message
--checkbox-background-errorSets the background color while checked and invalid, falling back to checkbox-error-color when unset
--checkbox-hint-colorSets the color of the hint message
--checkbox-sizeSets the size of the checkbox
--checkbox-border-colorSets the border color of the checkbox
--checkbox-border-radiusSets the corner radius of the box
--checkbox-focus-outline-colorSets the color of the focus outline used for keyboard navigation

Basic

Accept the terms and conditions
<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.

Disabled
<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.

Readonly, unchecked Readonly, checked
<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:

Select all Apple Banana Cherry
<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

Accept the terms and conditions
<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.

Custom checkbox
<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";
Email Phone SMS
<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

Email Phone SMS
<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":

Email Phone SMS

Disabled and readonly

Setting disabled or readonly on the list propagates it to every child checkbox, no need to repeat it on each one.

Email Phone
<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

Wi-Fi Shipping Insurance
<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.

Subscribe to the newsletter
<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.

Wi-Fi
<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.

Disabled Readonly Required
<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).

Wi-Fi Shipping Insurance
<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.

Custom card
<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 host
  • aria-checked="true"\|"false"\|"mixed": reflects checked, or "mixed" while indeterminate and unchecked
  • aria-disabled / aria-readonly: kept in sync with disabled / readonly
  • aria-required: kept in sync with required
  • aria-invalid: kept in sync with valid, once disabled or hideError are also accounted for
  • aria-description: set from hint/error content while present (via the inherited description), removed otherwise
  • aria-label: set from the label property 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 label property: eun-checkbox sets aria-label on 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 explicit aria-label is 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 icon is purely decorative (eun-icon renders its SVG with aria-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

WAI-ARIA Authoring Practices: Checkbox Pattern
WAI-ARIA Authoring Practices: Mixed Checkbox Pattern
WAI Web Accessibility Tutorials: Labeling Controls
WAI Web Accessibility Tutorials: Grouping Controls