A flexible checkbox group component that allows users to select multiple options from a set of choices. It provides a clean interface with support for labels, validation, different orientations, and comprehensive accessibility features.
import { CheckboxGroup } from 'frontile';
The most basic usage of the CheckboxGroup component with a label and options.
import { CheckboxGroup } from 'frontile';
<template>
<CheckboxGroup @label='Interests' as |Checkbox|>
<Checkbox @label='Music' />
<Checkbox @label='Sports' />
<Checkbox @label='Technology' />
</CheckboxGroup>
</template>
You can control individual checkbox states by providing @checked and handling updates with @onChange. This example uses an array-based data structure with a single reusable handler.
Selected (0):
{
"interests": []
}
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { fn } from '@ember/helper';
import { CheckboxGroup } from 'frontile';
export default class ControlledCheckboxGroup extends Component {
@tracked formData = {
interests: [] as string[]
};
// Helper to check if a value is selected
isChecked = (value: string) => {
return this.formData.interests?.includes(value) ?? false;
}
isChecked(value: string) {
return this.formData.interests?.includes(value) ?? false;
}
// Single reusable handler for checkbox changes
@action
handleCheckboxChange(value: string, checked: boolean) {
const current = this.formData.interests || [];
if (checked) {
this.formData = { ...this.formData, interests: [...current, value] };
} else {
this.formData = { ...this.formData, interests: current.filter((v) => v !== value) };
}
}
get selectedCount() {
return this.formData.interests?.length ?? 0;
}
get selectedItems() {
return this.formData.interests?.join(', ') ?? '';
}
<template>
<div class='flex flex-col gap-4'>
<CheckboxGroup @label='What are your interests?' as |Checkbox|>
<Checkbox
value='music'
@label='Music'
@checked={{this.isChecked 'music'}}
@onChange={{fn this.handleCheckboxChange 'music'}}
/>
<Checkbox
value='sports'
@label='Sports'
@checked={{this.isChecked 'sports'}}
@onChange={{fn this.handleCheckboxChange 'sports'}}
/>
<Checkbox
value='technology'
@label='Technology'
@checked={{this.isChecked 'technology'}}
@onChange={{fn this.handleCheckboxChange 'technology'}}
/>
<Checkbox
value='art'
@label='Art'
@checked={{this.isChecked 'art'}}
@onChange={{fn this.handleCheckboxChange 'art'}}
/>
</CheckboxGroup>
<div class='p-3 border border-neutral-soft rounded'>
<p class='text-sm'>
Selected ({{this.selectedCount}}):
{{this.selectedItems}}
</p>
</div>
<div class='p-4 bg-neutral-subtle rounded'>
<h4 class='font-medium mb-2'>Current Selection:</h4>
<pre class='text-sm overflow-auto'>{{JSON.stringify
this.formData
null
2
}}</pre>
</div>
</div>
</template>
}
The CheckboxGroup component integrates with form validation by displaying error messages and updating ARIA attributes accordingly.
Note: CheckboxGroup currently supports validation on form submit, but does not support automatic field-level validation (change/blur/input events) when used with form.Field. This differs from RadioGroup, which does support field-level validation. For CheckboxGroup, validation errors will only appear after the form is submitted.
This example demonstrates the recommended pattern for using CheckboxGroup with the Form component's validation system. It uses an array-based data structure with a single reusable handler for managing checkbox state.
{
"notificationMethods": []
}
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { fn } from '@ember/helper';
import { Form, CheckboxGroup, type FormResultData } from 'frontile';
import { Button } from 'frontile';
import * as v from 'valibot';
// Define validation schema with array of strings
const schema = v.object({
notificationMethods: v.pipe(
v.array(v.string()),
v.minLength(1, 'Please select at least one notification method')
)
});
type Schema = v.InferOutput<typeof schema>;
export default class ValidatedCheckboxGroup extends Component {
@tracked formData: Schema = {
notificationMethods: []
};
@tracked submitMessage = '';
// Helper to check if a value is selected
isChecked = (value: string) => {
return this.formData.notificationMethods?.includes(value) ?? false;
};
// Single reusable handler for checkbox changes
@action
handleCheckboxChange(value: string, checked: boolean) {
const current = this.formData.notificationMethods || [];
if (checked) {
this.formData.notificationMethods = [...current, value];
} else {
this.formData.notificationMethods = current.filter((v) => v !== value);
}
}
@action
handleFormChange(data: FormResultData<Schema>) {
this.formData = data.data;
}
@action
handleFormSubmit(data: FormResultData<Schema>) {
this.submitMessage = 'Preferences saved successfully!';
console.log('Submitted data:', data.data);
}
<template>
<div class='flex flex-col gap-4'>
<Form
@data={{this.formData}}
@schema={{schema}}
@onChange={{this.handleFormChange}}
@onSubmit={{this.handleFormSubmit}}
as |form|
>
<form.Field @name='notificationMethods' as |field|>
<field.CheckboxGroup
@label='Notification Preferences'
@description='Choose how you would like to receive notifications'
@isRequired={{true}}
as |Checkbox|
>
<Checkbox
value='email'
@label='Email notifications'
@checked={{this.isChecked 'email'}}
@onChange={{fn this.handleCheckboxChange 'email'}}
/>
<Checkbox
value='sms'
@label='SMS notifications'
@checked={{this.isChecked 'sms'}}
@onChange={{fn this.handleCheckboxChange 'sms'}}
/>
<Checkbox
value='push'
@label='Push notifications'
@checked={{this.isChecked 'push'}}
@onChange={{fn this.handleCheckboxChange 'push'}}
/>
</field.CheckboxGroup>
</form.Field>
<div>
<Button type='submit'>
Save Preferences
</Button>
</div>
</Form>
{{#if this.submitMessage}}
<div class='p-3 bg-success-subtle text-success-strong rounded'>
{{this.submitMessage}}
</div>
{{/if}}
<div class='p-4 bg-neutral-subtle rounded'>
<h4 class='font-medium mb-2'>Form Data:</h4>
<pre class='text-sm overflow-auto'>{{JSON.stringify
this.formData
null
2
}}</pre>
</div>
</div>
</template>
}
Here's a comprehensive example showing CheckboxGroup with the modern Form validation system, combining multiple CheckboxGroups with other input types. This example demonstrates array-based checkbox data and schema validation with both required and optional checkbox groups.
{
"name": "",
"interests": [],
"newsletters": []
}
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { fn } from '@ember/helper';
import { Form, CheckboxGroup, type FormResultData } from 'frontile';
import { Button } from 'frontile';
import * as v from 'valibot';
// Define validation schema
const schema = v.object({
name: v.pipe(
v.string(),
v.nonEmpty('Name is required'),
v.minLength(2, 'Name must be at least 2 characters')
),
interests: v.pipe(
v.array(v.string()),
v.minLength(1, 'Please select at least one interest')
),
newsletters: v.array(v.string()) // Optional - no minimum required
});
type Schema = v.InferOutput<typeof schema>;
export default class CompleteFormWithCheckbox extends Component {
@tracked formData: Schema = {
name: '',
interests: [],
newsletters: []
};
@tracked submitMessage = '';
// Helper to check if a value is selected in a field
isChecked = (field: 'interests' | 'newsletters', value: string) => {
return this.formData[field]?.includes(value) ?? false;
};
// Single reusable handler for any checkbox group
@action
handleCheckboxChange(field: 'interests' | 'newsletters', value: string, checked: boolean) {
const current = this.formData[field] || [];
if (checked) {
this.formData[field] = [...current, value];
} else {
this.formData[field] = current.filter((v) => v !== value);
}
}
@action
handleFormChange(data: FormResultData<Schema>) {
this.formData = data.data;
}
@action
handleFormSubmit(data: FormResultData<Schema>) {
this.submitMessage = 'Registration submitted successfully!';
console.log('Form submitted:', data.data);
}
<template>
<div class='flex flex-col gap-4'>
<Form
@data={{this.formData}}
@schema={{schema}}
@onChange={{this.handleFormChange}}
@onSubmit={{this.handleFormSubmit}}
as |form|
>
<div class='flex flex-col gap-4'>
<form.Field @name='name' as |field|>
<field.Input
@label='Full Name'
@isRequired={{true}}
/>
</form.Field>
<form.Field @name='interests' as |field|>
<field.CheckboxGroup
@label='Areas of Interest'
@description='Select topics you would like to hear about'
@isRequired={{true}}
@orientation='horizontal'
as |Checkbox|
>
<Checkbox
value='technology'
@label='Technology'
@checked={{this.isChecked 'interests' 'technology'}}
@onChange={{fn this.handleCheckboxChange 'interests' 'technology'}}
/>
<Checkbox
value='sports'
@label='Sports'
@checked={{this.isChecked 'interests' 'sports'}}
@onChange={{fn this.handleCheckboxChange 'interests' 'sports'}}
/>
<Checkbox
value='music'
@label='Music'
@checked={{this.isChecked 'interests' 'music'}}
@onChange={{fn this.handleCheckboxChange 'interests' 'music'}}
/>
<Checkbox
value='travel'
@label='Travel'
@checked={{this.isChecked 'interests' 'travel'}}
@onChange={{fn this.handleCheckboxChange 'interests' 'travel'}}
/>
</field.CheckboxGroup>
</form.Field>
<form.Field @name='newsletters' as |field|>
<field.CheckboxGroup
@label='Newsletter Subscriptions'
@description='Optional newsletters you can subscribe to'
as |Checkbox|
>
<Checkbox
value='weekly'
@label='Weekly Newsletter'
@description='Get the latest updates every week'
@checked={{this.isChecked 'newsletters' 'weekly'}}
@onChange={{fn this.handleCheckboxChange 'newsletters' 'weekly'}}
/>
<Checkbox
value='monthly'
@label='Monthly Digest'
@description='Monthly summary of important news and updates'
@checked={{this.isChecked 'newsletters' 'monthly'}}
@onChange={{fn this.handleCheckboxChange 'newsletters' 'monthly'}}
/>
</field.CheckboxGroup>
</form.Field>
<div>
<Button type='submit'>
Submit Registration
</Button>
</div>
</div>
</Form>
{{#if this.submitMessage}}
<div class='p-3 bg-success-subtle text-success-strong rounded'>
{{this.submitMessage}}
</div>
{{/if}}
<div class='p-4 bg-neutral-subtle rounded'>
<h4 class='font-medium mb-2'>Form Data:</h4>
<pre class='text-sm overflow-auto'>{{JSON.stringify
this.formData
null
2
}}</pre>
</div>
</div>
</template>
}
This example demonstrates various optional arguments and configurations available for CheckboxGroup:
@orientation='horizontal' to arrange checkboxes horizontally (default is vertical)@size='sm', @size='md', or @size='lg'@description, and to individual checkboxes with @description on each Checkbox@classes argumentimport { CheckboxGroup } from 'frontile';
import { hash } from '@ember/helper';
<template>
<div class='flex flex-col gap-6'>
{{! Horizontal Orientation }}
<CheckboxGroup
@label='Horizontal Layout'
@orientation='horizontal'
as |Checkbox|
>
<Checkbox @label='Option A' @description='First option in horizontal layout' />
<Checkbox @label='Option B' @description='Second option in horizontal layout' />
<Checkbox @label='Option C' @description='Third option in horizontal layout' />
</CheckboxGroup>
{{! Different Sizes }}
<div class='flex flex-col gap-4'>
<CheckboxGroup @label='Small Size' @size='sm' as |Checkbox|>
<Checkbox @label='Small Option A' @description='Compact checkbox size' />
<Checkbox @label='Small Option B' @description='Another compact size' />
</CheckboxGroup>
<CheckboxGroup @label='Medium Size' @size='md' as |Checkbox|>
<Checkbox @label='Medium Option A' @description='Standard checkbox size' />
<Checkbox @label='Medium Option B' @description='Another standard size' />
</CheckboxGroup>
<CheckboxGroup @label='Large Size' @size='lg' as |Checkbox|>
<Checkbox @label='Large Option A' @description='Spacious checkbox size' />
<Checkbox @label='Large Option B' @description='Another spacious size' />
</CheckboxGroup>
</div>
{{! With Group Description }}
<CheckboxGroup
@label='User Permissions'
@description='Select the permissions this user should have. You can modify these later.'
as |Checkbox|
>
<Checkbox @label='Read access' @description='View files and data' />
<Checkbox @label='Write access' @description='Create and edit content' />
<Checkbox @label='Delete access' @description='Remove files permanently' />
</CheckboxGroup>
{{! Custom Styling }}
<CheckboxGroup
@label='Custom Styled Group'
@classes={{hash
base='my-custom-checkbox-group'
label='my-custom-label'
optionsContainer='my-custom-options-container'
}}
as |Checkbox|
>
<Checkbox @label='Styled Option 1' @description='First custom styled option' />
<Checkbox @label='Styled Option 2' @description='Second custom styled option' />
<Checkbox @label='Styled Option 3' @description='Third custom styled option' />
</CheckboxGroup>
</div>
</template>
The CheckboxGroup component follows accessibility best practices:
aria-invalid)aria-describedbyElement: <span class="hljs-title class_">HTMLDivElement</span>
| Name | Type | Default | Description |
|---|---|---|---|
classes
|
SlotsToClasses<'label' | 'base' | 'optionsContainer'>
|
- | Class names for each slot of the component, merged with the theme's. |
description
|
string
|
- |
Help text rendered between the label and the control, and referenced by the
ids describedBy returns.
|
errors
|
enum
|
- |
Validation messages for the field. A non-empty value also marks the control
invalid, and an array is joined with ; when displayed.
|
isDisabled
|
boolean
|
false
|
Whether the field is disabled. FormControl passes this through for styling;
the control it wraps is responsible for the disabled attribute.
|
isInvalid
|
boolean
|
false
|
Marks the control invalid without supplying messages, for validation that is reported elsewhere. |
isRequired
|
boolean
|
false
|
Whether the field is required. Adds an asterisk to the label; it does not
set the required attribute on the control itself.
|
label
|
string
|
- |
The label text rendered above the control and associated with it via for.
Use the :label block instead when the label needs markup.
|
name
|
string
|
- | The name attribute applied to every checkbox in the group, so they are submitted together as one field. |
onChange
|
function
|
- |
Callback when a checkbox in the group is toggled. It is bound onto every
yielded Checkbox, so a checkbox that sets its own @onChange replaces this
one rather than running alongside it.
|
orientation
|
enum
|
'vertical'
|
How the checkboxes are laid out. |
size
|
enum
|
- | The size applied to every checkbox in the group and to the group's label. |
| Name | Type | Default | Description |
|---|---|---|---|
default
*
|
Array
|
- |