A customizable toggle control that allows users to switch between two states (on/off, enabled/disabled). The Switch component integrates seamlessly with the Form and Field components for automatic data binding and validation, providing a consistent and accessible experience. It supports custom content blocks for icons and labels.
Modern Usage: For most use cases, prefer using Switch with the Form and Field components. This provides automatic data binding, validation, and state management without manual
@onChangehandlers.
Field-Level Validation: Switch supports field-level validation that runs on change/blur events based on the Form's
@validateOnsetting, providing immediate feedback as users toggle the switch.
import { Switch } from 'frontile';
The most basic usage of a Switch component with a label.
import { Switch } from 'frontile';
<template><Switch @label='Enable Notifications' /></template>
The recommended pattern for using Switch is with Form and Field components, which provides automatic data binding and state management without manual onChange handlers.
Notifications are disabled
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Form, type FormResultData } from 'frontile';
export default class ControlledSwitch extends Component {
@tracked formData = { notifications: false };
handleFormChange = (result: FormResultData) => {
this.formData = result.data;
};
<template>
<div class='flex flex-col gap-4'>
<Form
@data={{this.formData}}
@onChange={{this.handleFormChange}}
as |form|
>
<form.Field @name='notifications' as |field|>
<field.Switch @label='Enable Email Notifications' />
</form.Field>
</Form>
<div class='p-3 border border-neutral-soft rounded'>
<p class='text-sm'>
Notifications are
<strong>{{if
this.formData.notifications
'enabled'
'disabled'
}}</strong>
</p>
</div>
</div>
</template>
}
Note: Switch supports field-level validation that runs on change/blur/input events based on the Form's @validateOn setting, providing immediate feedback as users interact with the switch.
The Switch component integrates with the Form validation system, providing automatic error display and field-level validation. This example demonstrates using Valibot schema validation with the Form/Field components.
{
"email": "",
"termsAccepted": false,
"notifications": true
}
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')
),
notifications: v.boolean()
});
type Schema = v.InferOutput<typeof schema>;
export default class ValidatedSwitch extends Component {
@tracked formData: Schema = {
email: '',
termsAccepted: false,
notifications: true
};
@tracked submitMessage = '';
@action
handleFormChange(data: FormResultData<Schema>) {
this.formData = data.data;
}
@action
handleFormSubmit(data: FormResultData<Schema>) {
this.submitMessage = 'Registration successful!';
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.Switch
@label='I accept the terms and conditions'
@description='You must accept our terms to continue'
@isRequired={{true}}
/>
</form.Field>
<form.Field @name='notifications' as |field|>
<field.Switch
@label='Send me email notifications'
@description='Optional: Receive updates about new features'
/>
</form.Field>
<Button type='submit'>
Register
</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>
}
Customize the switch appearance by using content blocks:
startContent: Renders before the switch thumb.thumbContent: Renders inside the switch thumb. It receives an object with isSelected to reflect the current state.endContent: Renders after the switch thumb.import Component from '@glimmer/component';
import { Switch } from 'frontile';
import { SunIcon, MoonIcon } from 'site/components/icons';
export default class CustomContentSwitchExample extends Component {
<template>
<Switch @label='Dark Mode'>
<:startContent>
<SunIcon />
</:startContent>
<:endContent>
<MoonIcon />
</:endContent>
</Switch>
</template>
}
import Component from '@glimmer/component';
import { Switch } from 'frontile';
import { SunIcon, MoonIcon } from 'site/components/icons';
export default class CustomContentSwitchExample extends Component {
<template>
<Switch @label='Dark Mode'>
<:thumbContent as |o|>
{{#if o.isSelected}}
<MoonIcon class='size-3' />
{{else}}
<SunIcon class='size-3' />
{{/if}}
</:thumbContent>
</Switch>
</template>
}
The Switch component supports various optional configurations for sizing, visual styling, states, and custom classes.
import { Switch } from 'frontile';
import { hash } from '@ember/helper';
<template>
<div class='flex flex-col gap-6'>
{{! Size variants }}
<div>
<h4 class='text-sm font-medium mb-2'>Size Variants</h4>
<div class='flex gap-4'>
<Switch @size='sm' @label='Small' />
<Switch @size='md' @label='Medium' />
<Switch @size='lg' @label='Large' />
</div>
</div>
{{! Intent variants }}
<div>
<h4 class='text-sm font-medium mb-2'>Intent Variants</h4>
<div class='flex gap-4'>
<Switch @intent='default' @label='Default' @defaultSelected={{true}} />
<Switch @intent='primary' @label='Primary' @defaultSelected={{true}} />
<Switch
@intent='secondary'
@label='Secondary'
@defaultSelected={{true}}
/>
<Switch
@intent='tertiary'
@label='Tertiary'
@defaultSelected={{true}}
/>
<Switch @intent='success' @label='Success' @defaultSelected={{true}} />
<Switch @intent='warning' @label='Warning' @defaultSelected={{true}} />
<Switch @intent='danger' @label='Danger' @defaultSelected={{true}} />
</div>
</div>
{{! Disabled state }}
<div>
<h4 class='text-sm font-medium mb-2'>Disabled State</h4>
<div class='flex flex-col gap-3'>
<Switch
@label='Disabled Switch (Off)'
@defaultSelected={{false}}
@isDisabled={{true}}
/>
<Switch
@label='Disabled Switch (On)'
@defaultSelected={{true}}
@isDisabled={{true}}
/>
<Switch
@label='Disabled with Description'
@description='This switch is disabled and cannot be toggled'
@defaultSelected={{true}}
@isDisabled={{true}}
/>
</div>
</div>
{{! Custom styling }}
<div>
<h4 class='text-sm font-medium mb-2'>Custom Styling</h4>
<Switch
@label='Custom Styled Switch'
@description='This switch has custom styling applied'
@classes={{hash
base='my-custom-switch-base'
wrapper='my-custom-switch-wrapper'
thumb='my-custom-switch-thumb'
label='my-custom-switch-label'
}}
/>
</div>
</div>
</template>
The Switch component follows accessibility best practices:
role="switch" behaviorfor and id attributesaria-invalid)aria-describedbyElement: <span class="hljs-title class_">HTMLInputElement</span>
| Name | Type | Default | Description |
|---|---|---|---|
classes
|
SlotsToClasses<'label' | 'base' | 'startContent' | 'endContent' | 'labelContainer' | 'wrapper' | 'hiddenInput' | 'thumb'>
|
- | Custom classes to style different slots of the Switch component. |
defaultSelected
|
boolean
|
false
|
Sets the initial selected state of the Switch when used in uncontrolled mode. |
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.
|
intent
|
enum
|
'primary'
|
The visual intent (e.g., color or style) of the Switch. |
isDisabled
|
boolean
|
- | Whether the Switch is disabled. When true, user interaction is prevented. |
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.
|
isSelected
|
boolean
|
- | Controls the current selected state of the Switch. When provided a boolean value, the component operates in a controlled mode. |
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 for the input element. Useful for form submissions. |
onBlur
|
function
|
- | Callback triggered when the Switch loses focus (blur event). |
onChange
|
function
|
- | Callback triggered when the Switch value changes. Receives the new boolean value and, optionally, the triggering Event. |
size
|
enum
|
- | The size of the Switch. |
| Name | Type | Default | Description |
|---|---|---|---|
startContent
*
|
Array
|
- | |
thumbContent
*
|
Array
|
- | |
endContent
*
|
Array
|
- |