bit-checkbox-group is a form-associated fieldset wrapper for bit-checkbox items. It owns the selection state for all child checkboxes, mirrors that state into the values attribute as a comma-separated string, and submits that string under the group's name in native forms and bit-form.
Features
- Form-associated group submission through
name - Comma-separated
valuesstate for preselection and controlled updates - Propagation of
color,size, anddisabledto childbit-checkboxelements fieldsetandlegendsemantics with helper and error text wiring- Vertical and horizontal layouts
Source Code
View Checkbox Group Source
ts
import {
define,
computed,
createContext,
createId,
effect,
html,
inject,
provide,
type ReadonlySignal,
signal,
} from '@vielzeug/craftit';
import { createChoiceFieldControl } from '@vielzeug/craftit/controls';
import type { ComponentSize, ThemeColor } from '../../types';
import { colorThemeMixin, disabledStateMixin, sizeVariantMixin } from '../../styles';
import { disablableBundle, sizableBundle, themableBundle, type PropBundle } from '../shared/bundles';
import { mountFormContextSync } from '../shared/dom-sync';
import { FORM_CTX } from '../shared/form-context';
import { createChoiceChangeDetail, 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;
/** 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 bit-checkbox elements */
size?: ComponentSize;
/** Comma-separated list of currently checked values */
values?: string;
};
export type BitCheckboxGroupEvents = {
change: ChoiceChangeDetail;
};
const checkboxGroupProps = {
...themableBundle,
...sizableBundle,
...disablableBundle,
error: '',
helper: '',
label: '',
name: '',
orientation: 'vertical',
required: false,
values: '',
} satisfies PropBundle<BitCheckboxGroupProps>;
/**
* 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} name - Form field name
* @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 = define<BitCheckboxGroupProps, BitCheckboxGroupEvents>('bit-checkbox-group', {
formAssociated: true,
props: checkboxGroupProps,
setup({ emit, host, props, slots }) {
const formCtx = inject(FORM_CTX, undefined);
mountFormContextSync(host.el, formCtx, props);
const choice = createChoiceFieldControl<string>({
context: formCtx,
disabled: props.disabled,
error: props.error,
getValue: (value) => value,
helper: props.helper,
label: props.label,
mapControlledValue: (value) => value,
multiple: signal(true),
name: props.name,
prefix: 'checkbox-group',
value: props.values,
});
const checkedValues = choice.selectedItems;
const getCheckboxes = (): HTMLElement[] =>
Array.from(host.el.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));
};
const toggleCheckbox = (val: string, originalEvent?: Event) => {
choice.toggleItem(val);
host.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 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.el.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(() => {
void slots.elements().value;
syncChildren();
});
effect(() => {
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 = 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],
});Basic Usage
html
<bit-checkbox-group label="Interests" values="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>Form Submission
Set name on the group when you want its selected values to submit with a form. The submitted value is a comma-separated string such as email,sms.
html
<bit-form id="prefs-form" novalidate>
<bit-checkbox-group name="contact" label="Preferred contact" values="email,sms">
<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'));
});
</script>Orientation
html
<bit-checkbox-group label="Notifications (vertical)">
<bit-checkbox value="email">Email</bit-checkbox>
<bit-checkbox value="push">Push</bit-checkbox>
<bit-checkbox value="sms">SMS</bit-checkbox>
</bit-checkbox-group>
<bit-checkbox-group label="Working days (horizontal)" orientation="horizontal" values="mon,wed,fri">
<bit-checkbox value="mon">Mon</bit-checkbox>
<bit-checkbox value="tue">Tue</bit-checkbox>
<bit-checkbox value="wed">Wed</bit-checkbox>
<bit-checkbox value="thu">Thu</bit-checkbox>
<bit-checkbox value="fri">Fri</bit-checkbox>
</bit-checkbox-group>Validation Feedback
html
<bit-checkbox-group label="Interests" helper="Select all that apply.">
<bit-checkbox value="sport">Sport</bit-checkbox>
<bit-checkbox value="music">Music</bit-checkbox>
<bit-checkbox value="travel">Travel</bit-checkbox>
</bit-checkbox-group>
<bit-checkbox-group label="Agreements" error="Please accept all required policies." color="error">
<bit-checkbox value="terms">I accept the terms of service</bit-checkbox>
<bit-checkbox value="privacy">I accept the privacy policy</bit-checkbox>
</bit-checkbox-group>API Reference
label:string, default''. Legend text used as the group's accessible name.values:string, default''. Comma-separated currently checked values.name:string, default''. Form field name used during submission.orientation:'vertical' | 'horizontal', default'vertical'. Layout direction of options.color:'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'. Propagated to child checkboxes.size:'sm' | 'md' | 'lg'. Propagated to child checkboxes.disabled:boolean, defaultfalse. Disables all checkboxes in the group.required:boolean, defaultfalse. Marks the group as required for accessibility and validation.error:string, default''. Error message shown below the group.helper:string, default''. Helper text shown below the group when no error is present.
Events
| Event | Detail | Description |
|---|---|---|
change | { values: string[] } | Full array of currently checked values after any toggle |
Best Practices
- Put
nameon the group instead of individual child checkboxes when you want one submitted field. - Update
values, not childcheckedattributes, when you want to control the group from outside. - Use
bit-radio-groupinstead when the user must pick exactly one option.