Diwa Design System

Checkbox

Checkboxes allow users to select one or more options from a set, or toggle a single binary choice. Supports indeterminate state for partial selection patterns.

When to use

Do

  • Use to let users select one or more independent options from a list.
  • Use a single checkbox for a binary opt-in / opt-out choice (e.g. "Remember me").
  • Use in forms where multiple answers may apply simultaneously.
  • Use the indeterminate state for "Select All" controls when only a subset is selected.
  • Use dense mode (compact) in tables, toolbars, or sidebars where space is constrained.

Don't

  • Don't use checkboxes for mutually exclusive options — use radio buttons instead.
  • Don't use a checkbox when the action takes effect immediately (use a toggle/switch).
  • Don't hide the label without providing an accessible screen-reader name via the label prop.
  • Don't apply error state without a visible message explaining what needs to be corrected.
  • Don't nest checkboxes more than one level deep — it creates confusing hierarchies.

Controlled pattern

diwa-checkbox is semi-controlled: it mutates its own checked prop on user interaction and simultaneously emits an update event. You can listen to update to keep external state in sync, or use the component standalone without any event wiring.

// Vanilla JS — listen for the update event
const el = document.querySelector('diwa-checkbox');
el.addEventListener('update', (e) => {
  console.log('checked:', e.detail.checked);
});

// React — use lowercase onupdate (React 19 custom element mapping)
<diwa-checkbox
  label="Accept terms"
  checked={accepted}
  onupdate={(e) => setAccepted(e.detail.checked)}
/>

Indeterminate — "Select All" pattern

The indeterminate state visually overrides checked and sets aria-checked="mixed" for screen readers. Use it for a parent "Select All" control when only some child options are selected. Clicking the indeterminate checkbox should select all; clicking again should deselect all.

const allChecked = items.every(Boolean);
const someChecked = items.some(Boolean) && !allChecked;

<diwa-checkbox
  label="Select all"
  checked={allChecked}
  indeterminate={someChecked}
  onupdate={(e) => {
    // If currently indeterminate → select all; otherwise reflect the request
    const nextChecked = someChecked ? true : e.detail.checked;
    setItems(items.map(() => nextChecked));
  }}
/>

Note: the component automatically clears the indeterminate attribute on user interaction (matching standard browser behaviour). The consumer should derive indeterminate from state, not set it imperatively.

Validation states

Set state="error" or state="success" together with a message to communicate feedback below the checkbox. The message is wired to the input via aria-describedby automatically.

<!-- Error — required field not checked -->
<diwa-checkbox
  label="I accept the terms"
  required
  state="error"
  message="You must accept the terms to continue."
/>

<!-- Success — validated -->
<diwa-checkbox
  label="Email notifications"
  checked
  state="success"
  message="Preference saved."
/>

Required fields

Setting required adds a visual asterisk to the label text and passes the attribute to the native <input>. Always pair requiredwith explicit error feedback — never rely on the browser's default validation popup.

<diwa-checkbox
  label="I agree to the privacy policy"
  required
  state={hasError ? 'error' : 'none'}
  message={hasError ? 'This field is required.' : ''}
/>

Form submission caveat (V1)

Known limitation: The inner <input name> lives inside Shadow DOM and is therefore not visible to an ancestor <form> for native form submission. This is a standard Shadow DOM caveat.

Workaround: Collect checkbox values via the update event and submit them programmatically (e.g. fetch / FormDataconstructed in JS). Native form participation via ElementInternals is planned for V2.