GitHub

Switch

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 @onChange handlers.

Field-Level Validation: Switch supports field-level validation that runs on change/blur events based on the Form's @validateOn setting, providing immediate feedback as users toggle the switch.

Import

import { Switch } from 'frontile';

Usage

Basic Switch

The most basic usage of a Switch component with a label.

import { Switch } from 'frontile';

<template><Switch @label='Enable Notifications' /></template>

Controlled with Form/Field

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>
}

Form Validation

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.

You must accept our terms to continue
Optional: Receive updates about new features

Form Data:

{
  "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>
}

With Icons

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>
}

Miscellaneous Options

The Switch component supports various optional configurations for sizing, visual styling, states, and custom classes.

Size Variants

Intent Variants

Disabled State

This switch is disabled and cannot be toggled

Custom Styling

This switch has custom styling applied
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>

Accessibility

The Switch component follows accessibility best practices:

  • Uses semantic checkbox input with role="switch" behavior
  • Proper label association using for and id attributes
  • ARIA attributes for invalid states (aria-invalid)
  • Descriptive text association using aria-describedby
  • Support for required field indication
  • Keyboard navigation (Space to toggle, Tab to focus)
  • Screen reader announcements for labels, descriptions, and validation messages
  • Visual focus indicators
  • Disabled state properly communicated to assistive technologies

API

Switch

Element: <span class="hljs-title class_">HTMLInputElement</span>

Arguments

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.

Blocks

Name Type Default Description
startContent * Array -
thumbContent * Array -
endContent * Array -
Released under MIT License - Created by Josemar Luedke