A standalone checkbox component that allows users to select or deselect individual options. While typically used within a CheckboxGroup, it can also be used individually when you need more control over the layout and behavior. The component is controlled, requiring @checked and @onChange for state management.
import { Checkbox } from 'frontile';
The most basic usage of a Checkbox component with a label.
import { Checkbox } from 'frontile';
<template><Checkbox @label='Subscribe to newsletter' /></template>
You can control the checkbox state by providing @checked and handling updates with @onChange.
Status: Not subscribed
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Checkbox } from 'frontile';
export default class ControlledCheckbox extends Component {
@tracked isSubscribed = false;
updateSubscription = (checked: boolean) => {
this.isSubscribed = checked;
};
<template>
<div class='flex flex-col gap-4'>
<Checkbox
@label='Subscribe to newsletter'
@checked={{this.isSubscribed}}
@onChange={{this.updateSubscription}}
/>
<div class='p-3 border border-neutral-soft rounded'>
<p class='text-sm'>
Status:
{{if this.isSubscribed 'Subscribed' 'Not subscribed'}}
</p>
</div>
</div>
</template>
}
When you need multiple independent checkboxes, each should have a unique name and state.
None selected
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Checkbox } from 'frontile';
export default class IndependentCheckboxes extends Component {
@tracked notifications = {
email: false,
sms: false,
push: false
};
updateEmailNotifications = (checked: boolean) => {
this.notifications = { ...this.notifications, email: checked };
};
updateSmsNotifications = (checked: boolean) => {
this.notifications = { ...this.notifications, sms: checked };
};
updatePushNotifications = (checked: boolean) => {
this.notifications = { ...this.notifications, push: checked };
};
get enabledNotifications() {
return Object.entries(this.notifications)
.filter(([_, enabled]) => enabled)
.map(([type]) => type)
.join(', ');
}
<template>
<div class='flex flex-col gap-4'>
<Checkbox
@name='email-notifications'
@label='Email notifications'
@checked={{this.notifications.email}}
@onChange={{this.updateEmailNotifications}}
/>
<Checkbox
@name='sms-notifications'
@label='SMS notifications'
@checked={{this.notifications.sms}}
@onChange={{this.updateSmsNotifications}}
/>
<Checkbox
@name='push-notifications'
@label='Push notifications'
@checked={{this.notifications.push}}
@onChange={{this.updatePushNotifications}}
/>
<div class='p-4 bg-neutral-subtle rounded'>
<h4 class='font-medium mb-2'>Enabled Notifications:</h4>
<p class='text-sm'>
{{if
this.enabledNotifications
this.enabledNotifications
'None selected'
}}
</p>
</div>
</div>
</template>
}
Note: This example shows manual state management. For forms with validation, see the "Form Validation" and "Complete Form Example" sections which use Form/Field components.
Control the size of checkboxes using the @size argument.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Checkbox } from 'frontile';
export default class CheckboxSizes extends Component {
@tracked smallChecked = false;
@tracked mediumChecked = false;
@tracked largeChecked = false;
updateSmallChecked = (checked: boolean) => {
this.smallChecked = checked;
};
updateMediumChecked = (checked: boolean) => {
this.mediumChecked = checked;
};
updateLargeChecked = (checked: boolean) => {
this.largeChecked = checked;
};
<template>
<div class='flex flex-col gap-4'>
<Checkbox
@name='small-checkbox'
@label='Small Checkbox'
@size='sm'
@checked={{this.smallChecked}}
@onChange={{this.updateSmallChecked}}
/>
<Checkbox
@name='medium-checkbox'
@label='Medium Checkbox'
@size='md'
@checked={{this.mediumChecked}}
@onChange={{this.updateMediumChecked}}
/>
<Checkbox
@name='large-checkbox'
@label='Large Checkbox'
@size='lg'
@checked={{this.largeChecked}}
@onChange={{this.updateLargeChecked}}
/>
</div>
</template>
}
Checkboxes can be disabled to prevent user interaction while maintaining their visual presence and current state.
import Component from '@glimmer/component';
import { Checkbox } from 'frontile';
export default class DisabledCheckbox extends Component {
<template>
<div class='flex flex-col gap-4'>
<Checkbox
@name='disabled-checked'
@label='Disabled Checked Checkbox'
@checked={{true}}
disabled={{true}}
/>
<Checkbox
@name='disabled-unchecked'
@label='Disabled Unchecked Checkbox'
@checked={{false}}
disabled={{true}}
/>
<Checkbox
@name='disabled-with-description'
@label='Disabled Checkbox with Description'
@description='This checkbox is disabled and cannot be modified'
@checked={{true}}
disabled={{true}}
/>
</div>
</template>
}
The indeterminate state represents a "partially checked" state, commonly used for "select all" checkboxes when only some child items are selected. Since HTML doesn't support setting indeterminate via an attribute, you need to use a modifier to set it via JavaScript.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { modifier } from 'ember-modifier';
import { Checkbox } from 'frontile';
export default class IndeterminateCheckbox extends Component {
@tracked items = [
{ label: 'Email notifications', checked: true },
{ label: 'SMS notifications', checked: false },
{ label: 'Push notifications', checked: true }
];
get allChecked() {
return this.items.every((item) => item.checked);
}
get noneChecked() {
return this.items.every((item) => !item.checked);
}
get isIndeterminate() {
return !this.allChecked && !this.noneChecked;
}
setIndeterminate = modifier(
(element: HTMLInputElement, [indeterminate]: [boolean]) => {
element.indeterminate = indeterminate;
}
);
toggleAll = (checked: boolean) => {
this.items = this.items.map((item) => ({ ...item, checked }));
};
toggleItem = (index: number) => (checked: boolean) => {
this.items = this.items.map((item, i) =>
i === index ? { ...item, checked } : item
);
};
<template>
<div class='flex flex-col gap-2'>
<Checkbox
@name='select-all'
@label='Select All'
@checked={{this.allChecked}}
@onChange={{this.toggleAll}}
{{this.setIndeterminate this.isIndeterminate}}
/>
<div class='ml-6 flex flex-col gap-2'>
{{#each this.items as |item index|}}
<Checkbox
@name='item-{{index}}'
@label={{item.label}}
@checked={{item.checked}}
@onChange={{this.toggleItem index}}
/>
{{/each}}
</div>
</div>
</template>
}
Note: The
indeterminateproperty can only be set via JavaScript — there is no HTML attribute for it. Use theember-modifierpackage to create a modifier that setselement.indeterminateon the checkbox input.
Add helpful description text that appears below the checkbox label.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Checkbox } from 'frontile';
export default class CheckboxWithDescription extends Component {
@tracked termsAccepted = false;
@tracked marketingConsent = false;
updateTermsAccepted = (checked: boolean) => {
this.termsAccepted = checked;
};
updateMarketingConsent = (checked: boolean) => {
this.marketingConsent = checked;
};
<template>
<div class='flex flex-col gap-4'>
<Checkbox
@name='terms-acceptance'
@label='I agree to the terms and conditions'
@description='By checking this box, you agree to our terms of service, privacy policy, and cookie policy. You can review these documents at any time.'
@checked={{this.termsAccepted}}
@onChange={{this.updateTermsAccepted}}
/>
<Checkbox
@name='marketing-consent'
@label='I consent to receive marketing communications'
@description='We will send you occasional updates about new features, promotions, and company news. You can unsubscribe at any time.'
@checked={{this.marketingConsent}}
@onChange={{this.updateMarketingConsent}}
/>
</div>
</template>
}
Note: When used with field.Checkbox as demonstrated in the examples below, individual Checkbox components support field-level validation (validates on change/blur/input events based on @validateOn). This differs from CheckboxGroup, which currently only supports validation on form submit.
The Checkbox component integrates with the Form validation system, providing automatic error display and field-level validation. This example demonstrates using a required checkbox with the Form/Field components.
{
"termsAccepted": false
}
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { Form, type FormResultData } from 'frontile';
import { Button } from 'frontile';
import { array } from '@ember/helper';
import * as v from 'valibot';
// Define validation schema - checkbox must be true
const schema = v.object({
termsAccepted: v.pipe(
v.boolean(),
v.literal(true, 'You must accept the terms and conditions to continue')
)
});
type Schema = v.InferOutput<typeof schema>;
export default class ValidatedCheckbox extends Component {
@tracked formData: Schema = {
termsAccepted: false
};
@tracked submitMessage = '';
@action
handleFormChange(data: FormResultData<Schema>) {
this.formData = data.data;
}
@action
handleFormSubmit(data: FormResultData<Schema>) {
this.submitMessage = 'Terms accepted 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}}
@validateOn={{array 'change' 'submit'}}
as |form|
>
<form.Field @name='termsAccepted' as |field|>
<field.Checkbox
@label='I accept the terms and conditions'
@description='Please read and accept our terms and conditions before proceeding.'
@isRequired={{true}}
/>
</form.Field>
<div>
<Button type='submit'>
Continue
</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>
}
Create a horizontal layout using CSS Grid or Flexbox.
Note: For horizontal layouts of related checkboxes, consider using
CheckboxGroupwith@orientation='horizontal'.
Selected preferences:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Checkbox } from 'frontile';
export default class HorizontalCheckboxes extends Component {
@tracked preferences = {
newsletter: false,
updates: false,
promotions: false
};
updatePreference = (key: string) => (checked: boolean) => {
this.preferences = { ...this.preferences, [key]: checked };
};
<template>
<div class='flex flex-col gap-4'>
<fieldset>
<legend class='text-lg font-medium mb-3'>Email Preferences</legend>
<div class='flex gap-4'>
<Checkbox
@name='newsletter-pref'
@label='Newsletter'
@checked={{this.preferences.newsletter}}
@onChange={{this.updatePreference 'newsletter'}}
/>
<Checkbox
@name='updates-pref'
@label='Product Updates'
@checked={{this.preferences.updates}}
@onChange={{this.updatePreference 'updates'}}
/>
<Checkbox
@name='promotions-pref'
@label='Promotions'
@checked={{this.preferences.promotions}}
@onChange={{this.updatePreference 'promotions'}}
/>
</div>
</fieldset>
<div class='p-3 border border-neutral-soft rounded'>
<p class='text-sm'>
Selected preferences:
{{#each-in this.preferences as |key value|}}
{{#if value}}{{key}} {{/if}}
{{/each-in}}
</p>
</div>
</div>
</template>
}
Here's a comprehensive example showing Checkbox components integrated with the Form validation system. This example demonstrates a registration form with an Input field, a required checkbox, and an optional checkbox.
{
"email": "",
"termsAccepted": false,
"marketingConsent": false
}
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { Form, type FormResultData } from 'frontile';
import { Button } from 'frontile';
import { array } from '@ember/helper';
import * as v from 'valibot';
// Define validation schema
const schema = v.object({
email: v.pipe(
v.string(),
v.nonEmpty('Email is required'),
v.email('Please enter a valid email address')
),
termsAccepted: v.pipe(
v.boolean(),
v.literal(true, 'You must accept the terms and conditions')
),
marketingConsent: v.boolean() // Optional checkbox - no literal(true) requirement
});
type Schema = v.InferOutput<typeof schema>;
export default class CompleteFormWithCheckbox extends Component {
@tracked formData: Schema = {
email: '',
termsAccepted: false,
marketingConsent: false
};
@tracked submitMessage = '';
@action
handleFormChange(data: FormResultData<Schema>) {
this.formData = data.data;
}
@action
handleFormSubmit(data: FormResultData<Schema>) {
this.submitMessage = 'Account created 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}}
@validateOn={{array 'change' 'submit'}}
as |form|
>
<div class='flex flex-col gap-4'>
<form.Field @name='email' as |field|>
<field.Input
@label='Email Address'
@type='email'
@isRequired={{true}}
/>
</form.Field>
<form.Field @name='termsAccepted' as |field|>
<field.Checkbox
@label='I agree to the Terms and Conditions'
@description='You must accept our terms to create an account'
@isRequired={{true}}
/>
</form.Field>
<form.Field @name='marketingConsent' as |field|>
<field.Checkbox
@label='I would like to receive marketing emails'
@description='Optional: Get updates about new features and promotions'
/>
</form.Field>
<div>
<Button type='submit' disabled={{form.isSubmitting}}>
{{if form.isSubmitting 'Creating Account...' 'Create Account'}}
</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>
}
You can customize the appearance of different parts of the checkbox using the @classes argument.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Checkbox } from 'frontile';
import { hash } from '@ember/helper';
export default class CustomStyledCheckbox extends Component {
@tracked customChecked = false;
updateCustomChecked = (checked: boolean) => {
this.customChecked = checked;
};
<template>
<Checkbox
@name='custom-styled'
@label='Custom Styled Checkbox'
@description='This checkbox has custom styling applied'
@checked={{this.customChecked}}
@onChange={{this.updateCustomChecked}}
@classes={{hash
base='my-custom-checkbox-base'
input='my-custom-checkbox-input'
labelContainer='my-custom-label-container'
label='my-custom-checkbox-label'
}}
/>
</template>
}
Use the standalone Checkbox component when:
Use CheckboxGroup when:
The Checkbox component follows accessibility best practices:
for and id attributesaria-invalid)aria-describedbyElement: <span class="hljs-title class_">HTMLInputElement</span>
| Name | Type | Default | Description |
|---|---|---|---|
checked
|
boolean
|
- |
Whether the checkbox is checked. Pair with onChange to control the
checkbox; leave it unset to let the element track its own state.
|
classes
|
SlotsToClasses<'label' | 'base' | 'input' | 'labelContainer'>
|
- | 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 of the underlying input, used when the checkbox is submitted as part of a form. |
onBlur
|
function
|
- | Callback when onblur is triggered |
onChange
|
function
|
- | Callback when onchange is triggered |
size
|
enum
|
'md'
|
The size of the checkbox and its label. |