Skip to content

Checkbox

A customizable boolean form control with indeterminate state support, plus a group wrapper for managing multi-selection lists.

  • sg-checkbox — standalone checkbox for a single boolean value.
  • sg-checkbox-group — form-associated <fieldset> wrapper that manages a set of checkboxes, propagates color, size, and disabled to all children, and tracks checked values as a comma-separated values string.

Features

Checkbox

  • Accessiblearia-checked including "mixed" for indeterminate; keyboard toggle
  • 6 Semantic Colors — primary, secondary, info, success, warning, error
  • Indeterminate State — partial selection indicator for "select all" patterns
  • States — checked, unchecked, indeterminate, disabled
  • 3 Sizes — sm, md, lg
  • Customizable — CSS custom properties for size, radius, and colors

Checkbox Group

  • 2 Orientations — vertical & horizontal
  • Validation Feedbackhelper and error text with ARIA wiring
  • Form Integration — comma-separated values submit through the group's name with any <form> or sg-form
  • Context Propagationcolor, size, and disabled propagate to all child checkboxes
  • Semantic Markup — renders as <fieldset> + <legend> for proper screen reader grouping

Source Code

View Checkbox Source
ts
import { define, useField, html, inject, prop } from '@vielzeug/craft';
import { computed } from '@vielzeug/ripple';

import type { CheckableProps, ComponentSize, ThemeColor } from '../../types';

import { type CheckableChangePayload, lifecycleSignal, createCheckable } from '../../headless';
import '../../content/icon/icon';
import { CONTROL_SIZE_PRESET, disablableBundle, sizableBundle, themableBundle } from '../../shared';
import {
  coarsePointerMixin,
  colorThemeMixin,
  disabledStateMixin,
  forcedColorsFormControlMixin,
  sizeVariantMixin,
} from '../../styles';
import { CHECKBOX_GROUP_CTX } from '../checkbox-group/checkbox-group';
import { applyCheckableBinding } from '../shared/field-binding';
import { FORM_CTX, useFormContext } from '../shared/form-context';
import { renderHelperRegion } from '../shared/templates';
import componentStyles from './checkbox.css?inline';

export type SgCheckboxEvents = {
  change: CheckableChangePayload;
};

export type SgCheckboxProps = CheckableProps & {
  /** Theme color */
  color?: ThemeColor;
  /** Disable interaction */
  disabled?: boolean;
  /** Error message (marks field as invalid) */
  error?: string;
  /** Helper text displayed below the checkbox */
  helper?: string;
  /** Indeterminate state (partially checked) */
  indeterminate?: boolean;
  /** Component size */
  size?: ComponentSize;
};

/**
 * A customizable checkbox component with theme colors, sizes, and indeterminate state support.
 *
 * @element sg-checkbox
 *
 * @attr {boolean} checked - Checked state
 * @attr {boolean} disabled - Disable checkbox interaction
 * @attr {boolean} indeterminate - Indeterminate (partially checked) state
 * @attr {string} value - Field value submitted with forms
 * @attr {string} name - Form field name
 * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'
 * @attr {string} size - Checkbox size: 'sm' | 'md' | 'lg'
 * @attr {string} error - Error message (marks field as invalid)
 * @attr {string} helper - Helper text displayed below the checkbox
 *
 * @fires change - Emitted when checkbox is toggled. detail: { checked: boolean, value: string, originalEvent?: Event }
 *
 * @slot - Checkbox label text
 *
 * @cssprop --checkbox-size - Control size (width and height)
 * @cssprop --checkbox-radius - Control border radius
 * @cssprop --checkbox-bg - Unchecked background color
 * @cssprop --checkbox-border-color - Unchecked border color
 * @cssprop --checkbox-checked-bg - Checked/indeterminate background color
 * @cssprop --checkbox-color - Checkmark icon color
 * @cssprop --checkbox-font-size - Label font size
 * @part checkbox - The checkbox wrapper element
 * @part box - The visual checkbox box
 * @part label - The label element
 * @part helper-text - The helper/error text element
 *
 * @example
 * ```html
 * <sg-checkbox name="agree" required>I agree to the terms</sg-checkbox>
 * <sg-checkbox checked color="primary" size="lg">Enabled by default</sg-checkbox>
 * <sg-checkbox error="This field is required" helper="Check to continue">Accept</sg-checkbox>
 * ```
 */
export const CHECKBOX_TAG = 'sg-checkbox' as const;
define<SgCheckboxProps, SgCheckboxEvents>(CHECKBOX_TAG, {
  formAssociated: true,
  props: {
    ...themableBundle,
    ...sizableBundle,
    ...disablableBundle,
    checked: prop.bool(false),
    error: prop.string(),
    helper: prop.string(),
    indeterminate: prop.bool(false),
    name: prop.string(),
    value: prop.string('on'),
  },
  setup(props, { bind, emit, onCleanup }) {
    const formCtx = inject(FORM_CTX);
    const fCtxProps = useFormContext(bind, props, formCtx);
    const groupCtx = inject(CHECKBOX_GROUP_CTX);

    let _formField: { reportValidity(): void } | null = null;
    const checkable = createCheckable({
      checked: props.checked,
      clearIndeterminateFirst: true,
      disabled: computed(() => fCtxProps.disabled.value || Boolean(groupCtx?.disabled.value)),
      error: props.error,
      getFormField: () => _formField,
      group: groupCtx,
      helper: props.helper,
      indeterminate: props.indeterminate,
      onToggle: (payload) => {
        checkable.triggerValidation('change');

        // In a checkbox-group, the group owns change emission/state updates.
        // Emitting here would bubble to the group and toggle a second time.
        if (groupCtx) return;

        emit('change', payload);
      },
      prefix: 'checkbox',
      signal: lifecycleSignal(onCleanup),
      validateOn: formCtx?.validateOn,
      value: props.value,
    });

    _formField = useField<string | null>({
      disabled: checkable.disabled,
      toFormValue: (v) => v,
      value: checkable.checkableFormValue,
    });

    const {
      assistiveId,
      checked,
      disabled,
      errorText,
      handleClick,
      handleKeydown,
      helperText,
      indeterminate,
      labelId,
    } = checkable;

    applyCheckableBinding(
      bind,
      fCtxProps.size,
      { assistiveId, checked, disabled, errorText, handleClick, handleKeydown, helperText, indeterminate, labelId },
      'checkbox',
    );

    return html`
      <div class="checkbox-wrapper" part="checkbox">
        <div class="box" part="box">
          <sg-icon class="checkmark" name="check" size="14" stroke-width="2" aria-hidden="true"></sg-icon>
          <sg-icon class="dash" name="minus" size="14" stroke-width="2" aria-hidden="true"></sg-icon>
        </div>
      </div>
      <span class="label" part="label" id="${labelId}"><slot></slot></span>
      ${renderHelperRegion(assistiveId, errorText, helperText)}
    `;
  },
  styles: [
    colorThemeMixin,
    forcedColorsFormControlMixin,
    disabledStateMixin,
    coarsePointerMixin,
    sizeVariantMixin(CONTROL_SIZE_PRESET),
    componentStyles,
  ],
});
View Checkbox Group Source
ts
import { createContext, createStableId, define, useField, html, inject, prop, when } from '@vielzeug/craft';
import { computed, type ReadonlySignal, signal } from '@vielzeug/ripple';

import type { ComponentSize, ThemeColor } from '../../types';

import {
  type ChoiceChangeDetail,
  lifecycleSignal,
  createChoiceField,
  getChoiceLabel,
  getLightChildrenByTag,
} from '../../headless';
import { disablableBundle, sizableBundle, themableBundle } from '../../shared';
import { colorThemeMixin, disabledStateMixin, sizeVariantMixin } from '../../styles';
import { FORM_CTX, useFormContext } from '../shared/form-context';
import componentStyles from './checkbox-group.css?inline';

// ─── Context ──────────────────────────────────────────────────────────────────

export type CheckboxGroupContext = {
  color: ReadonlySignal<ThemeColor | undefined>;
  disabled: ReadonlySignal<boolean>;
  size: ReadonlySignal<ComponentSize | undefined>;
  toggle: (value: string, originalEvent?: Event) => void;
  values: ReadonlySignal<string[]>;
};

export const CHECKBOX_GROUP_CTX = createContext<CheckboxGroupContext>('CheckboxGroupContext');

// ─── Types ────────────────────────────────────────────────────────────────────

export type SgCheckboxGroupProps = {
  /** Theme color — propagated to all child sg-checkbox elements */
  color?: ThemeColor;
  /** Disable all checkboxes in the group */
  disabled?: boolean;
  /** Error message shown below the group */
  error?: string;
  /** Helper text shown below the group */
  helper?: string;
  /** Legend / label for the fieldset. Required for accessibility. */
  label?: string;
  /** Form field name used during submission */
  name?: string;
  /** Layout direction of the checkbox options */
  orientation?: 'vertical' | 'horizontal';
  /** Mark the group as required */
  required?: boolean;
  /** Size — propagated to all child sg-checkbox elements */
  size?: ComponentSize;
  /** Comma-separated list of currently checked values */
  values?: string;
};

export type SgCheckboxGroupEvents = {
  change: ChoiceChangeDetail;
};

/**
 * A fieldset wrapper that groups `sg-checkbox` elements, provides shared
 * `color` and `size` via context, and manages multi-value selection state.
 *
 * @element sg-checkbox-group
 *
 * @attr {string} label - Legend text (required for a11y)
 * @attr {string} values - Comma-separated list of checked values
 * @attr {boolean} disabled - Disable all checkboxes in the group
 * @attr {string} error - Error message
 * @attr {string} helper - Helper text
 * @attr {string} name - Form field name
 * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'
 * @attr {string} size - Component size: 'sm' | 'md' | 'lg'
 * @attr {string} orientation - Layout: 'vertical' | 'horizontal'
 * @attr {boolean} required - Required field
 *
 * @fires change - Emitted when selection changes. detail: { value: string, values: string[], labels: string[], originalEvent?: Event }
 *
 * @slot - Place `sg-checkbox` elements here
 *
 * @cssprop --checkbox-group-direction - Flex direction of the items list ('row' | 'column')
 * @cssprop --checkbox-group-gap - Gap between checkbox items
 * @part items - Items container.
 * @example
 * ```html
 * <sg-checkbox-group name="fruits" label="Favourite fruits" required>
 *   <sg-checkbox value="apple">Apple</sg-checkbox>
 *   <sg-checkbox value="banana">Banana</sg-checkbox>
 *   <sg-checkbox value="cherry">Cherry</sg-checkbox>
 * </sg-checkbox-group>
 * <sg-checkbox-group name="options" orientation="horizontal" color="primary">
 *   <sg-checkbox value="a">Option A</sg-checkbox>
 *   <sg-checkbox value="b">Option B</sg-checkbox>
 * </sg-checkbox-group>
 * ```
 */
export const CHECKBOX_GROUP_TAG = 'sg-checkbox-group' as const;
define<SgCheckboxGroupProps, SgCheckboxGroupEvents>(CHECKBOX_GROUP_TAG, {
  formAssociated: true,
  props: {
    ...themableBundle,
    ...sizableBundle,
    ...disablableBundle,
    error: prop.string(),
    helper: prop.string(),
    label: prop.string(),
    name: prop.string(),
    orientation: prop.string('vertical'),
    required: prop.bool(false),
    values: prop.string(),
  },
  setup(props, { bind, el, emit, onCleanup, provide, slots, watch }) {
    const formCtx = inject(FORM_CTX);
    const fCtxProps = useFormContext(bind, props, formCtx);

    let _formField: { reportValidity(): void } | null = null;
    const choice = createChoiceField({
      disabled: fCtxProps.disabled,
      error: props.error,
      getFormField: () => _formField,
      helper: props.helper,
      multiple: signal(true),
      prefix: 'checkbox-group',
      signal: lifecycleSignal(onCleanup),
      validateOn: formCtx?.validateOn,
      value: props.values,
    });

    _formField = useField<string>({ disabled: choice.disabled, toFormValue: (v) => v, value: choice.formValue });

    const checkedValues = choice.selectedValues;

    const getCheckboxes = (): HTMLElement[] => getLightChildrenByTag(el, 'sg-checkbox');
    const getLabelForValue = (value: string): string => getChoiceLabel(getCheckboxes(), value);
    const emitChange = (originalEvent?: Event) => {
      const values = checkedValues.value;

      const labels = values.map(getLabelForValue);

      emit('change', { labels, originalEvent, values });
    };

    const toggleCheckbox = (val: string, originalEvent?: Event) => {
      choice.toggleValue(val);
      el.setAttribute('values', choice.formValue.value);
      choice.triggerValidation('change');
      emitChange(originalEvent);
    };

    provide(CHECKBOX_GROUP_CTX, {
      color: props.color,
      disabled: computed(() => Boolean(props.disabled.value)),
      size: props.size,
      toggle: toggleCheckbox,
      values: checkedValues,
    });

    // Sync checked state + color/size/disabled onto slotted sg-checkbox children
    const syncChildren = () => {
      const values = checkedValues.value;
      const color = props.color.value;
      const size = props.size.value;
      const disabled = props.disabled.value;
      const checkboxes = getCheckboxes();

      for (const checkbox of checkboxes) {
        const val = checkbox.getAttribute('value') ?? '';

        checkbox.toggleAttribute('checked', values.includes(val));

        if (color) checkbox.setAttribute('color', color);
        else checkbox.removeAttribute('color');

        if (size) checkbox.setAttribute('size', size);
        else checkbox.removeAttribute('size');

        checkbox.toggleAttribute('disabled', Boolean(disabled));
      }
    };

    watch(() => {
      void slots.elements().value;
      syncChildren();
    });

    watch(() => {
      void slots.elements().value;

      const listeners = getCheckboxes().map((checkbox) => {
        const handler = (event: Event) => {
          event.stopPropagation();

          const val = (checkbox.getAttribute('value') ?? '').trim();

          if (!val) return;

          toggleCheckbox(val, event);
        };

        checkbox.addEventListener('change', handler);

        return () => {
          checkbox.removeEventListener('change', handler);
        };
      });

      return () => {
        for (const dispose of listeners) dispose();
      };
    });

    const legendId = createStableId('checkbox-group-legend');
    const errorId = `${legendId}-error`;
    const helperId = `${legendId}-helper`;
    const hasError = () => Boolean(props.error.value);
    const hasHelper = () => Boolean(props.helper.value) && !hasError();

    bind({ attr: { size: fCtxProps.size } });

    return html`
      <fieldset
        role="group"
        aria-required="${() => String(Boolean(props.required.value))}"
        aria-invalid="${() => String(hasError())}"
        aria-errormessage="${() => (hasError() ? errorId : null)}"
        aria-describedby="${() => (hasError() ? errorId : hasHelper() ? helperId : null)}">
        <legend id="${legendId}" ?hidden=${() => !props.label.value}>
          ${props.label}${when(
            () => Boolean(props.required.value),
            () => html`<span aria-hidden="true"> *</span>`,
          )}
        </legend>
        <div class="checkbox-group-items" part="items">
          <slot></slot>
        </div>
        <div class="error-text" id="${errorId}" role="alert" ?hidden=${() => !hasError()}>${props.error}</div>
        <div class="helper-text" id="${helperId}" ?hidden=${() => !hasHelper()}>${props.helper}</div>
      </fieldset>
    `;
  },
  styles: [colorThemeMixin, sizeVariantMixin(), disabledStateMixin, componentStyles],
});

Checkbox

Basic Usage

html
<sg-checkbox>Accept terms and conditions</sg-checkbox>

Colors

Six semantic colors to match your design language or validation state.

PreviewCode
RTL

Sizes

PreviewCode
RTL

Indeterminate

Use the indeterminate state for "select all" controls where only some items in a sub-list are checked. First click resolves to checked; subsequent clicks toggle normally.

PreviewCode
RTL

States

Disabled

PreviewCode
RTL

Helper & Error Text

Provide contextual feedback directly below the checkbox.

PreviewCode
RTL

Listening for Changes

js
const checkbox = document.querySelector('sg-checkbox');
checkbox.addEventListener('change', (e) => {
  console.log('checked:', e.detail.checked);
  console.log('value:', e.detail.value);
});

Checkbox Group

sg-checkbox-group wraps sg-checkbox elements in a <fieldset>. Set values to a comma-separated string to pre-select options, and set name when you want the group to submit with a form.

Basic Usage

html
<sg-checkbox-group label="Interests" values="sport,music">
  <sg-checkbox value="sport">Sport</sg-checkbox>
  <sg-checkbox value="music">Music</sg-checkbox>
  <sg-checkbox value="travel">Travel</sg-checkbox>
</sg-checkbox-group>

Orientation

PreviewCode
RTL

Colors & Sizes

color and size set on the group propagate automatically to all child checkboxes.

PreviewCode
RTL

Validation Feedback

PreviewCode
RTL

Disabled

Disabling the group propagates to all child checkboxes.

PreviewCode
RTL

Form Integration

The group's checked values are stored in the values attribute and submitted under the group's name as a comma-separated string with any <form> or sg-form.

PreviewCode
RTL

Select All Pattern

Combine indeterminate state on a parent checkbox with a sg-checkbox-group to build a "select all" control.

PreviewCode
RTL

API Reference

sg-checkbox Attributes

AttributeTypeDefaultDescription
checkedbooleanfalseChecked state
indeterminatebooleanfalseIndeterminate (partially checked) state
disabledbooleanfalseDisable interaction
valuestring'on'Value submitted with the form
namestring''Form field name
color'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'Semantic color for the checked state
size'sm' | 'md' | 'lg''md'Checkbox size
helperstring''Helper text displayed below
errorstring''Error message (marks field invalid)

sg-checkbox Slots

SlotDescription
(default)Checkbox label text

sg-checkbox Parts

PartDescription
checkboxThe checkbox wrapper element
boxThe visual checkbox square
labelThe label text element

sg-checkbox Events

EventDetailDescription
change{ checked: boolean, value: string }Emitted when the checked state changes

sg-checkbox CSS Custom Properties

PropertyDescription
--checkbox-sizeCheckbox dimensions
--checkbox-radiusBorder radius
--checkbox-bgBackground color (unchecked state)
--checkbox-checked-bgBackground color (checked state)
--checkbox-border-colorBorder color
--checkbox-colorCheckmark icon color
--checkbox-font-sizeLabel font size

sg-checkbox-group Attributes

AttributeTypeDefaultDescription
labelstring''Legend text — required for accessibility
valuesstring''Comma-separated currently checked values (e.g. "a,b")
namestring''Form field name
orientation'vertical' | 'horizontal''vertical'Layout direction of options
color'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'Color propagated to all child checkboxes
size'sm' | 'md' | 'lg'Size propagated to all child checkboxes
disabledbooleanfalseDisable all checkboxes in the group
requiredbooleanfalseMark the group as required
errorstring''Error message shown below the group (also sets ARIA invalid)
helperstring''Helper text (hidden when error is set)

sg-checkbox-group Slots

SlotDescription
(default)Place sg-checkbox elements here

sg-checkbox-group Events

EventDetailDescription
change{ values: string[] }Full array of currently checked values after any toggle

Accessibility

The checkbox components follow WCAG 2.1 Level AA standards.

sg-checkbox

Keyboard Navigation
  • Space / Enter toggle the focused checkbox; Tab moves focus in and out.
Screen Readers
  • Uses role="checkbox" with aria-checked set to "true", "false", or "mixed" for indeterminate.
  • aria-labelledby links the label; aria-describedby links helper text; aria-errormessage links error text.
  • aria-disabled reflects the disabled state.

sg-checkbox-group

Semantic Structure
  • Renders as a <fieldset> with a <legend> for the label attribute.
Keyboard Navigation
  • Tab moves to the next checkbox within the group.
Screen Readers
  • aria-required and aria-invalid reflect the group validation state; aria-errormessage and aria-describedby link the text nodes.

Best Practices

Do:

  • Always provide a meaningful label on the group — it is the accessible name read before each option.
  • Use indeterminate on a "select all" checkbox to represent partial selection.
  • Use orientation="horizontal" only for short option labels that comfortably fit on one line.
  • Pair error with color="error" to reinforce validation failures visually.
  • Prefer name on the group (not individual checkboxes) when submitting with a form.

Don't:

  • Use sg-checkbox-group for mutually exclusive choices — use sg-radio-group instead.
  • Omit the label attribute on the group; without it the fieldset has no accessible name.
  • Place non-sg-checkbox elements as direct children of sg-checkbox-group.