Checkbox
A customizable boolean form control with indeterminate state support, plus a group wrapper for managing multi-selection lists.
bit-checkbox— standalone checkbox for a single boolean value.bit-checkbox-group—<fieldset>wrapper that manages a set of checkboxes, propagatescolor,size, anddisabledto all children, and tracks checked values as a comma-separated string.
Features
Checkbox
- ♿ Accessible —
aria-checkedincluding"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 Feedback —
helperanderrortext with ARIA wiring - 📝 Form Integration — comma-separated
valuesubmits with any<form>orbit-form - 📡 Context Propagation —
color,size, anddisabledpropagate to all child checkboxes - 🗂️ Semantic Markup — renders as
<fieldset>+<legend>for proper screen reader grouping
Source Code
View Checkbox Source
import { defineComponent, html, inject, signal, watch } from '@vielzeug/craftit';
import { useA11yControl, createCheckableControl } from '@vielzeug/craftit/labs';
import type { CheckableProps, DisablableProps, SizableProps, ThemableProps } from '../../types';
import { coarsePointerMixin, formControlMixins, sizeVariantMixin } from '../../styles';
import { CHECKBOX_GROUP_CTX } from '../checkbox-group/checkbox-group';
import { useToggleField } from '../shared/composables';
import { CONTROL_SIZE_PRESET } from '../shared/design-presets';
import { mountFormContextSync } from '../shared/dom-sync';
import componentStyles from './checkbox.css?inline';
export type BitCheckboxEvents = {
change: { checked: boolean; fieldValue: string; originalEvent?: Event; value: boolean };
};
export type BitCheckboxProps = CheckableProps &
ThemableProps &
SizableProps &
DisablableProps & {
/** Error message (marks field as invalid) */
error?: string;
/** Helper text displayed below the checkbox */
helper?: string;
/** Indeterminate state (partially checked) */
indeterminate?: boolean;
};
/**
* A customizable checkbox component with theme colors, sizes, and indeterminate state support.
*
* @element bit-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: { value: boolean, checked: boolean, fieldValue: string, originalEvent?: Event }
*
* @slot - Checkbox label text
*
* @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
*/
export const CHECKBOX_TAG = defineComponent<BitCheckboxProps, BitCheckboxEvents>({
formAssociated: true,
props: {
checked: { default: false },
color: { default: undefined },
disabled: { default: false },
error: { default: '' },
helper: { default: '' },
indeterminate: { default: false },
name: { default: '' },
size: { default: undefined },
value: { default: 'on' },
},
setup({ emit, host, props, reflect }) {
// Form integration — provides checkedSignal and triggerValidation
const { checkedSignal, formCtx, triggerValidation } = useToggleField(props);
mountFormContextSync(host, formCtx, props);
// Separate writable indeterminate signal, synced from the prop
const indeterminateSignal = signal(Boolean(props.indeterminate.value));
watch(props.indeterminate, (v) => {
indeterminateSignal.value = Boolean(v);
});
const groupCtx = inject(CHECKBOX_GROUP_CTX, undefined);
// Pass the writable checkedSignal directly — toggle() mutates it in place
const controlHandle = createCheckableControl({
checked: checkedSignal,
clearIndeterminateFirst: true,
disabled: props.disabled,
group: groupCtx,
indeterminate: indeterminateSignal,
onToggle: (e) => {
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', controlHandle.changePayload(e));
},
value: props.value,
});
const a11y = useA11yControl(host, {
checked: () => {
if (controlHandle.indeterminate.value) return 'mixed';
return controlHandle.checked.value ? 'true' : 'false';
},
helperText: () => props.error.value || props.helper.value,
helperTone: () => (props.error.value ? 'error' : 'default'),
invalid: () => !!props.error.value,
role: 'checkbox',
});
reflect({
checked: () => controlHandle.checked.value,
classMap: () => ({
'is-checked': controlHandle.checked.value,
'is-disabled': !!props.disabled.value,
'is-indeterminate': controlHandle.indeterminate.value,
}),
indeterminate: () => controlHandle.indeterminate.value,
onClick: (e: Event) => controlHandle.toggle(e),
onKeydown: (e: Event) => {
const ke = e as KeyboardEvent;
if (ke.key === ' ' || ke.key === 'Enter') {
ke.preventDefault();
controlHandle.toggle(e);
}
},
tabindex: () => (props.disabled.value ? undefined : 0),
});
return html`
<div class="checkbox-wrapper" part="checkbox">
<div class="box" part="box">
<svg
class="checkmark"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
xmlns="http://www.w3.org/2000/svg">
<path d="M 20,6 9,17 4,12" />
</svg>
<svg
class="dash"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
xmlns="http://www.w3.org/2000/svg">
<path d="M 5,12 H 19" />
</svg>
</div>
</div>
<span class="label" part="label" data-a11y-label id="${a11y.labelId}"><slot></slot></span>
<div
class="helper-text"
part="helper-text"
data-a11y-helper
id="${a11y.helperId}"
aria-live="polite"
hidden></div>
`;
},
styles: [...formControlMixins, coarsePointerMixin, sizeVariantMixin(CONTROL_SIZE_PRESET), componentStyles],
tag: 'bit-checkbox',
});View Checkbox Group Source
import {
computed,
createContext,
createId,
defineComponent,
effect,
handle,
html,
inject,
onMount,
onSlotChange,
provide,
type ReadonlySignal,
signal,
} from '@vielzeug/craftit';
import type { ComponentSize, ThemeColor } from '../../types';
import { colorThemeMixin, disabledStateMixin, sizeVariantMixin } from '../../styles';
import { mountFormContextSync } from '../shared/dom-sync';
import { FORM_CTX } from '../shared/form-context';
import { createChoiceChangeDetail, parseCsvValues, type ChoiceChangeDetail } from '../shared/utils';
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 BitCheckboxGroupProps = {
/** Theme color — propagated to all child bit-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;
/** Layout direction of the checkbox options */
orientation?: 'vertical' | 'horizontal';
/** Mark the group as required */
required?: boolean;
/** Size — propagated to all child bit-checkbox elements */
size?: ComponentSize;
/** Comma-separated list of currently checked values */
values?: string;
};
export type BitCheckboxGroupEvents = {
change: ChoiceChangeDetail;
};
/**
* A fieldset wrapper that groups `bit-checkbox` elements, provides shared
* `color` and `size` via context, and manages multi-value selection state.
*
* @element bit-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} color - Theme color
* @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 `bit-checkbox` elements here
*/
export const CHECKBOX_GROUP_TAG = defineComponent<BitCheckboxGroupProps, BitCheckboxGroupEvents>({
props: {
color: { default: undefined },
disabled: { default: false },
error: { default: '' },
helper: { default: '' },
label: { default: '' },
orientation: { default: 'vertical' },
required: { default: false },
size: { default: undefined },
values: { default: '' },
},
setup({ emit, host, props }) {
// Parse comma-separated value string into an array of checked values
const parseValues = (v: string | undefined): string[] => parseCsvValues(v);
const checkedValues = signal<string[]>(parseValues(props.values.value));
const getCheckboxes = (): HTMLElement[] => Array.from(host.getElementsByTagName('bit-checkbox')) as HTMLElement[];
const getLabelForValue = (value: string): string => {
const checkbox = getCheckboxes().find((item) => (item.getAttribute('value') ?? '') === value);
return checkbox?.textContent?.replace(/\s+/g, ' ').trim() || value;
};
const emitChange = (originalEvent?: Event) => {
const values = checkedValues.value;
emit('change', createChoiceChangeDetail(values, values.map(getLabelForValue), originalEvent));
};
// Keep checkedValues in sync when prop changes externally
effect(() => {
checkedValues.value = parseValues(props.values.value);
});
const toggleCheckbox = (val: string, originalEvent?: Event) => {
const current = checkedValues.value;
const next = current.includes(val) ? current.filter((v) => v !== val) : [...current, val];
checkedValues.value = next;
host.setAttribute('values', next.join(','));
emitChange(originalEvent);
};
const formCtx = inject(FORM_CTX, undefined);
mountFormContextSync(host, formCtx, props);
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 bit-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 = Array.from(host.getElementsByTagName('bit-checkbox')) as HTMLElement[];
for (const checkbox of checkboxes) {
const val = checkbox.getAttribute('value') ?? '';
if (values.includes(val)) checkbox.setAttribute('checked', '');
else checkbox.removeAttribute('checked');
if (color) checkbox.setAttribute('color', color);
else checkbox.removeAttribute('color');
if (size) checkbox.setAttribute('size', size);
else checkbox.removeAttribute('size');
if (disabled) checkbox.setAttribute('disabled', '');
else checkbox.removeAttribute('disabled');
}
};
effect(syncChildren);
onMount(() => {
onSlotChange('default', syncChildren);
syncChildren();
handle(host, 'change', (e: Event) => {
if (e.target === host) return;
e.stopPropagation();
const val = (e.target as HTMLElement).getAttribute('value') ?? '';
toggleCheckbox(val, e);
});
});
const legendId = createId('checkbox-group-legend');
const errorId = `${legendId}-error`;
const helperId = `${legendId}-helper`;
const hasError = computed(() => Boolean(props.error.value));
const hasHelper = computed(() => Boolean(props.helper.value) && !hasError.value);
return html`
<fieldset
role="group"
aria-required="${() => String(Boolean(props.required.value))}"
aria-invalid="${() => String(hasError.value)}"
aria-errormessage="${() => (hasError.value ? errorId : null)}"
aria-describedby="${() => (hasError.value ? errorId : hasHelper.value ? helperId : null)}">
<legend id="${legendId}" ?hidden=${() => !props.label.value}>
${() => props.label.value}${() => (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.value}>
${() => props.error.value}
</div>
<div class="helper-text" id="${helperId}" ?hidden=${() => !hasHelper.value}>${() => props.helper.value}</div>
</fieldset>
`;
},
styles: [colorThemeMixin, sizeVariantMixin(), disabledStateMixin(), componentStyles],
tag: 'bit-checkbox-group',
});Checkbox
Basic Usage
<bit-checkbox>Accept terms and conditions</bit-checkbox>
<script type="module">
import '@vielzeug/buildit/checkbox';
</script>Colors
Six semantic colors to match your design language or validation state.
Sizes
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.
States
Disabled
Helper & Error Text
Provide contextual feedback directly below the checkbox.
Listening for Changes
const checkbox = document.querySelector('bit-checkbox');
checkbox.addEventListener('change', (e) => {
console.log('checked:', e.detail.checked);
console.log('value:', e.detail.value);
});Checkbox Group
bit-checkbox-group wraps bit-checkbox elements in a <fieldset>. Set value to a comma-separated string to pre-select options.
Basic Usage
<bit-checkbox-group label="Interests" value="sport,music">
<bit-checkbox value="sport">Sport</bit-checkbox>
<bit-checkbox value="music">Music</bit-checkbox>
<bit-checkbox value="travel">Travel</bit-checkbox>
</bit-checkbox-group>
<script type="module">
import '@vielzeug/buildit/checkbox-group';
import '@vielzeug/buildit/checkbox';
</script>Orientation
Colors & Sizes
color and size set on the group propagate automatically to all child checkboxes.
Validation Feedback
Disabled
Disabling the group propagates to all child checkboxes.
Form Integration
The group's checked values are stored as a comma-separated value attribute and submitted with any <form> or bit-form.
<bit-form id="prefs-form" novalidate>
<bit-checkbox-group name="contact" label="Preferred contact" required>
<bit-checkbox value="email">Email</bit-checkbox>
<bit-checkbox value="phone">Phone</bit-checkbox>
<bit-checkbox value="sms">SMS</bit-checkbox>
</bit-checkbox-group>
<bit-button type="submit">Save Preferences</bit-button>
</bit-form>
<script type="module">
import '@vielzeug/buildit/form';
import '@vielzeug/buildit/checkbox-group';
import '@vielzeug/buildit/checkbox';
import '@vielzeug/buildit/button';
document.getElementById('prefs-form').addEventListener('submit', (e) => {
console.log('contact:', e.detail.formData.get('contact'));
});
document.querySelector('bit-checkbox-group').addEventListener('change', (e) => {
console.log('Checked values:', e.detail.values);
});
</script>Select All Pattern
Combine indeterminate state on a parent checkbox with a bit-checkbox-group to build a "select all" control.
<bit-checkbox id="select-all" indeterminate>Select all</bit-checkbox>
<bit-checkbox-group id="options" label="Options" value="a">
<bit-checkbox value="a">Option A</bit-checkbox>
<bit-checkbox value="b">Option B</bit-checkbox>
<bit-checkbox value="c">Option C</bit-checkbox>
</bit-checkbox-group>
<script type="module">
import '@vielzeug/buildit/checkbox';
import '@vielzeug/buildit/checkbox-group';
const all = document.getElementById('select-all');
const group = document.getElementById('options');
const options = ['a', 'b', 'c'];
function syncParent() {
const checked = group.getAttribute('value')?.split(',').filter(Boolean) ?? [];
if (checked.length === 0) {
all.removeAttribute('checked');
all.removeAttribute('indeterminate');
} else if (checked.length === options.length) {
all.setAttribute('checked', '');
all.removeAttribute('indeterminate');
} else {
all.removeAttribute('checked');
all.setAttribute('indeterminate', '');
}
}
all.addEventListener('change', (e) => {
if (e.detail.checked) {
group.setAttribute('value', options.join(','));
} else {
group.setAttribute('value', '');
}
syncParent();
});
group.addEventListener('change', syncParent);
syncParent();
</script>API Reference
bit-checkbox Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
checked | boolean | false | Checked state |
indeterminate | boolean | false | Indeterminate (partially checked) state |
disabled | boolean | false | Disable interaction |
value | string | 'on' | Value submitted with the form |
name | string | '' | Form field name |
color | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error' | — | Semantic color for the checked state |
size | 'sm' | 'md' | 'lg' | 'md' | Checkbox size |
helper | string | '' | Helper text displayed below |
error | string | '' | Error message (marks field invalid) |
bit-checkbox Slots
| Slot | Description |
|---|---|
| (default) | Checkbox label text |
bit-checkbox Parts
| Part | Description |
|---|---|
checkbox | The checkbox wrapper element |
box | The visual checkbox square |
label | The label text element |
bit-checkbox Events
| Event | Detail | Description |
|---|---|---|
change | { checked: boolean, value: string } | Emitted when the checked state changes |
bit-checkbox CSS Custom Properties
| Property | Description |
|---|---|
--checkbox-size | Checkbox dimensions |
--checkbox-radius | Border radius |
--checkbox-bg | Background color (unchecked state) |
--checkbox-checked-bg | Background color (checked state) |
--checkbox-border-color | Border color |
--checkbox-color | Checkmark icon color |
--checkbox-font-size | Label font size |
bit-checkbox-group Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
label | string | '' | Legend text — required for accessibility |
value | string | '' | Comma-separated currently checked values (e.g. "a,b") |
name | string | '' | 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 |
disabled | boolean | false | Disable all checkboxes in the group |
required | boolean | false | Mark the group as required |
error | string | '' | Error message shown below the group (also sets ARIA invalid) |
helper | string | '' | Helper text (hidden when error is set) |
bit-checkbox-group Slots
| Slot | Description |
|---|---|
| (default) | Place bit-checkbox elements here |
bit-checkbox-group Events
| Event | Detail | Description |
|---|---|---|
change | { values: string[] } | Full array of currently checked values after any toggle |
Accessibility
The checkbox components follow WCAG 2.1 Level AA standards.
bit-checkbox
✅ Keyboard Navigation
Space/Entertoggle the focused checkbox;Tabmoves focus in and out.
✅ Screen Readers
- Uses
role="checkbox"witharia-checkedset to"true","false", or"mixed"for indeterminate. aria-labelledbylinks the label;aria-describedbylinks helper text;aria-errormessagelinks error text.aria-disabledreflects the disabled state.
bit-checkbox-group
✅ Semantic Structure
- Renders as a
<fieldset>with a<legend>for thelabelattribute.
✅ Keyboard Navigation
Tabmoves to the next checkbox within the group.
✅ Screen Readers
aria-requiredandaria-invalidreflect the group validation state;aria-errormessageandaria-describedbylink the text nodes.
Best Practices
Do:
- Always provide a meaningful
labelon the group — it is the accessible name read before each option. - Use
indeterminateon a "select all" checkbox to represent partial selection. - Use
orientation="horizontal"only for short option labels that comfortably fit on one line. - Pair
errorwithcolor="error"to reinforce validation failures visually. - Prefer
nameon the group (not individual checkboxes) when submitting with a form.
Don't:
- Use
bit-checkbox-groupfor mutually exclusive choices — usebit-radio-groupinstead. - Omit the
labelattribute on the group; without it the fieldset has no accessible name. - Place non-
bit-checkboxelements as direct children ofbit-checkbox-group.