sg-checkbox-group is a form-associated fieldset wrapper for sg-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 sg-form.
Features
- Form-associated group submission through
name - Comma-separated
valuesstate for preselection and controlled updates - Propagation of
color,size, anddisabledto childsg-checkboxelements fieldsetandlegendsemantics with helper and error text wiring- Vertical and horizontal layouts
Source Code
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],
});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>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
<sg-form id="prefs-form" novalidate>
<sg-checkbox-group name="contact" label="Preferred contact" values="email,sms">
<sg-checkbox value="email">Email</sg-checkbox>
<sg-checkbox value="phone">Phone</sg-checkbox>
<sg-checkbox value="sms">SMS</sg-checkbox>
</sg-checkbox-group>
<sg-button type="submit">Save Preferences</sg-button>
</sg-form>
<script type="module">
import '@vielzeug/sigil/form';
import '@vielzeug/sigil/checkbox-group';
import '@vielzeug/sigil/checkbox';
import '@vielzeug/sigil/button';
document.getElementById('prefs-form').addEventListener('submit', (e) => {
console.log('contact:', e.detail.formData.get('contact'));
});
</script>Orientation
html
<sg-checkbox-group label="Notifications (vertical)">
<sg-checkbox value="email">Email</sg-checkbox>
<sg-checkbox value="push">Push</sg-checkbox>
<sg-checkbox value="sms">SMS</sg-checkbox>
</sg-checkbox-group>
<sg-checkbox-group label="Working days (horizontal)" orientation="horizontal" values="mon,wed,fri">
<sg-checkbox value="mon">Mon</sg-checkbox>
<sg-checkbox value="tue">Tue</sg-checkbox>
<sg-checkbox value="wed">Wed</sg-checkbox>
<sg-checkbox value="thu">Thu</sg-checkbox>
<sg-checkbox value="fri">Fri</sg-checkbox>
</sg-checkbox-group>Validation Feedback
html
<sg-checkbox-group label="Interests" helper="Select all that apply.">
<sg-checkbox value="sport">Sport</sg-checkbox>
<sg-checkbox value="music">Music</sg-checkbox>
<sg-checkbox value="travel">Travel</sg-checkbox>
</sg-checkbox-group>
<sg-checkbox-group label="Agreements" error="Please accept all required policies." color="error">
<sg-checkbox value="terms">I accept the terms of service</sg-checkbox>
<sg-checkbox value="privacy">I accept the privacy policy</sg-checkbox>
</sg-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
sg-radio-groupinstead when the user must pick exactly one option.