Skip to content

HySwitch ​

Usage examples on this page are written for Lit / plain HTML (<hy-switch>). The same component ships as HySwitch in @whitespaceux/harmony-react (native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.

A form-associated switch component that toggles between on and off states. Provides comprehensive form integration via the ElementInternals API, accessibility features, and customizable styling.

A field or a settings row? ​

A field has its label above it, or beside it with layout="inline" (its helper and messages then sit under the control). A settings row — a setting's name with a line of description at the start and the control at the row's end, as on a preferences page — is a row of your own: a flex row with justify-content: space-between, the name and description as your own text, and the control named with aria-labelledby pointing at the name, with no label, helper-text or layout. Rebuilding a drawn settings page, keep the control at the row's end.

Key Behavioral Model ​

This switch implements a clear separation between user interactions and programmatic control:

User Interactions (Mouse clicks, Space key) ​

  • Binary states: Toggle between off ↔ on
  • Standard behavior: Follows native switch/toggle conventions
  • Predictable UX: Users get consistent toggle behavior

Programmatic Control (JavaScript methods) ​

  • Method-controlled: Use toggle(), turnOn(), turnOff()
  • Event tracking: All changes include reason field ('user' | 'programmatic')

Form Integration ​

This component implements the Form-Associated Custom Elements API, allowing it to participate in form submission just like native checkboxes. The switch's value will be included in FormData when in the "on" state and the form is submitted.

Usage Guidelines ​

Switches are ideal for settings that take effect immediately and represent binary on/off states. Use switches for enabling/disabling features, toggling preferences, or controlling visibility. For selections that require a form submission, consider using checkboxes instead.

Sizes ​

  • small: Compact switch for dense layouts or inline controls
  • medium (default): Standard switch size for most use cases
  • large: Prominent switch for emphasis or improved touch targets

Value states ​

One line shows under the switch: the error message, else the warning message, else the helper text.

  • error (error / error-message): not accepted — danger track, aria-invalid
  • warning (warning / warning-message): accepted but worth a second look — warning track, never aria-invalid Neither touches form validity — that comes from required.

States ​

  • off: Inactive state (default)
  • on: Active/enabled state
  • disabled: Non-interactive state (can be combined with off/on)

Keyboard Navigation ​

  • Space key: Toggles between off ↔ on
  • Tab: Moves focus to/from the switch
  • Enter: No effect (standard switch behavior - doesn't change state)

When to use

  • A setting that takes effect the moment it changes: notifications on, dark mode, auto-save.
  • A list of independent settings, one switch each.

Do

  • Label the setting, not the state: "Email notifications", not "On".

When not to use

  • A choice that applies only after save or submit: hy-checkbox.
  • Accepting terms or giving consent: hy-checkbox in the form, recorded on submit.
  • More than two options, or switches that would exclude each other: hy-radio-group or hy-select.
  • A pressed tool in a toolbar (bold, mute): hy-toggle.

Accessibility: your part ​

  • Name the setting, not its states ("Email notifications", never "On"). The name must read right in both positions.
  • Show the switch's effect close to it, so people can see what changed.

Examples ​

Basic toggle ​

html
<hy-switch name="notifications" value="enabled" label="Enable notifications"> </hy-switch>

Form submission ​

html
<form>
  <hy-switch name="dark-mode" value="enabled" label="Dark mode"> </hy-switch>
  <button type="submit">Save</button>
</form>

Label before the switch — a row of switches read label first ​

html
<hy-switch label="Disabled" label-placement="start" size="small"></hy-switch>
<hy-switch label="Loading" label-placement="start" size="small"></hy-switch>

With top label and help text ​

html
<hy-switch
  name="newsletter"
  value="subscribed"
  top-label="Newsletter Preferences"
  label="Subscribe to weekly newsletter"
  helper-text="Get the latest updates delivered to your inbox"
>
</hy-switch>

Required field with an error message ​

html
<form>
  <hy-switch
    name="terms"
    value="accepted"
    label="Accept terms and conditions"
    error-message="You must accept to continue"
    required
  >
  </hy-switch>
  <button type="submit">Submit</button>
</form>

Warning message ​

html
<hy-switch
  name="auto-delete"
  label="Delete files after 30 days"
  state="on"
  warning-message="Deleted files cannot be recovered"
>
</hy-switch>

Different sizes ​

html
<hy-switch size="small" label="Small switch"></hy-switch>
<hy-switch size="medium" label="Medium switch"></hy-switch>
<hy-switch size="large" label="Large switch"></hy-switch>

Disabled states ​

html
<hy-switch label="Disabled off" disabled></hy-switch>
<hy-switch label="Disabled on" state="on" disabled></hy-switch>

Programmatic control ​

javascript
const switchElement = document.querySelector('hy-switch');

// Turn on
switchElement.turnOn();

// Turn off
switchElement.turnOff();

// Toggle
switchElement.toggle();

// Check state
console.log(switchElement.checked); // true/false
console.log(switchElement.state); // 'on'/'off'

// Form validation
if (switchElement.checkValidity()) {
  console.log('Valid');
} else {
  switchElement.reportValidity();
}

Event handling ​

javascript
switchElement.addEventListener('change', (e) => {
  console.log({
    state: e.detail.state, // 'on' or 'off'
    checked: e.detail.checked, // true or false
    value: e.detail.value, // switch's value attribute
    reason: e.detail.reason, // 'user' or 'programmatic'
  });

  // Save setting immediately
  if (e.detail.reason === 'user') {
    savePreference(e.detail.checked);
  }
});

Complete settings interface ​

html
<form>
  <fieldset>
    <legend>Notification Preferences</legend>

    <hy-switch
      name="email-notifications"
      value="enabled"
      label="Email notifications"
      helper-text="Receive updates via email"
      state="on"
    >
    </hy-switch>

    <hy-switch
      name="push-notifications"
      value="enabled"
      label="Push notifications"
      helper-text="Receive push notifications in browser"
      state="on"
    >
    </hy-switch>

    <hy-switch
      name="sms-notifications"
      value="enabled"
      label="SMS notifications"
      helper-text="Receive text message alerts"
      disabled
    >
    </hy-switch>
  </fieldset>

  <button type="submit">Save Preferences</button>
</form>

Programmatic Methods ​

javascript
const switchEl = document.querySelector('hy-switch');

// State control
switchEl.turnOn();
switchEl.turnOff();
switchEl.toggle();

// Form validation
switchEl.checkValidity();
switchEl.reportValidity();

// Focus management
switchEl.focus();
switchEl.blur();

// Access form
console.log(switchEl.form);
console.log(switchEl.validity);
console.log(switchEl.validationMessage);

API ​

Properties ​

PropertyAttributeTypeDefaultDescription
statestate'off' | 'on''off'The current state of the switch ('off' or 'on').
labellabelstring''The text label displayed next to the switch.
iconOnicon-onstring'check'The icon in the thumb while the switch is on, a name from the design system's icon family; '' draws none. The default, check, is the design system's glyph: whether it shows is its setting (--hy-switch-thumb-icon-on-display, shown by default). Any other name is the instance's own (a theme switch's moon) and always shows. Named icon-on (not on-icon): Svelte reads any on… attribute as an event handler.
iconOfficon-offstring'close'The icon in the thumb while the switch is off. The default, close, is the design system's glyph and hidden unless its setting shows it (--hy-switch-thumb-icon-off-display); any other name (a theme switch's sun) always shows; '' draws none.
helperTexthelper-textstring''Helper text displayed below the switch. An error or warning message replaces it.
topLabeltop-labelstring''Top label displayed above the switch, useful for grouping or section headers.
disableddisabledbooleanfalseDisables the switch, making it non-interactive.
requiredrequiredbooleanfalseMakes the switch required for form submission (must be in 'on' state).
namenamestring''The name of the switch for form submission.
valuevaluestring'on'The value submitted with the form when the switch is 'on'.
errorerrorbooleanfalseError state: the value is not accepted. Paints the danger color and sets aria-invalid="true"; it never blocks submission (validity comes from required). Set on its own it colors the switch and keeps the helper text — for a message shown elsewhere. Setting error-message raises it.
errorMessageerror-messagestring''Error message shown under the switch (replaces the helper text). Setting it raises error, clearing it lowers it.
warningwarningbooleanfalseWarning state: the value is accepted but worth a second look. Paints the warning color and shows warning-message; never sets aria-invalid and never blocks submission. error wins when both are set.
warningMessagewarning-messagestring''Warning message shown under the switch (replaces the helper text). Setting it raises warning.
sizesize'small' | 'medium' | 'large''medium'The size of the switch ('small', 'medium', or 'large').
layoutlayout'block' | 'inline''block'Row layout: block (default) stacks topLabel above the switch; inline puts topLabel on the left of the row and the switch on the right. Helper text or message always sits below the row. Only meaningful when topLabel is set. Set --hy-switch-label-column-width on a wrapping container so stacked switches align.
labelPlacementlabel-placement'start' | 'end''end'Which side of the switch label sits on: end (default, after it) or start (before it). Logical, so it flips in right-to-left text; the gap is the same on both sides. Has no effect on top-label.
ariaDescribedbyaria-describedbystring''Additional element IDs to include in aria-describedby for accessibility.
switchIdswitch-idstring''Id for the switch input element; empty uses the fixed input (components.md "Ids").
helperTextIdhelper-text-idstring''Id for the helper text element; empty uses the fixed helper.
topLabelIdtop-label-idstring''Id for the top label element; empty uses the fixed top-label.
autofocusautofocusbooleanfalseMarks this switch as the auto-focus target. Read by hy-dialog (and other overlay containers) on show to choose initial focus. Also focuses on first connect when used outside an overlay, mirroring native HTML autofocus but routed through JS so SSR'd HTML doesn't pull focus during hydration.
checkedcheckedboolean—Boolean convenience property that mirrors the state property. Returns true when state is 'on', false when 'off'. Setting this property will update the state accordingly.
form—HTMLFormElement | null—Returns the form element that contains this switch, if any.
validity—ValidityState—Returns the ValidityState object for the switch.
validationMessage—string—Returns the validation message for the switch.

Events ​

EventDetailDescription
change—Fired when the state changes (the form's change event); detail: state, checked, value and reason
input—Standard form input event

CSS Parts ​

PartDescription
baseThe switch's base wrapper element
helper-textThe helper text element
error-messageThe error message element
warning-messageThe warning message element
message-iconThe icon starting the error or warning message

CSS Custom Properties ​

The documented hooks. Every override hook the component reads — one per token it paints — is listed in @whitespaceux/harmony/theme-api.json, and @whitespaceux/harmony/component-bindings.json (the Harmony MCP get-bindings tool) says which part and state each one paints.

PropertyDefaultDescription
--hy-switch-text-transformnoneText transform for switch label
--hy-switch-thumb-icon-on-displayflexWhether the thumb's default glyph (icon-on, a check) shows while the switch is on: flex (shown, the default) or none. A glyph an instance names itself always shows; under forced colors the on glyph always shows.
--hy-switch-thumb-icon-off-displaynoneWhether the thumb's default glyph (icon-off, a cross) shows while the switch is off: flex or none (hidden, the default). Both shown is the two-glyph switch; both hidden the blank thumb.
--hy-switch-shadow-elevation-1var(--hy-shadow-elevation-1)Box shadow for the switch thumb at rest
--hy-switch-shadow-elevation-2var(--hy-shadow-elevation-2)Box shadow for the switch thumb on hover
--hy-switch-label-md-font-familyvar(--hy-label-md-font-family)Font family override for the top label (the switch label and the hint read their body and caption roles whole)
--hy-switch-label-column-width—Width of the label column in layout="inline" mode. No default — set on a wrapping container so stacked switches align.
--hy-switch-background-danger-tint-lightvar(--hy-background-danger-tint-light)Off track in error state.
--hy-switch-background-warning-tint-lightvar(--hy-background-warning-tint-light)Off track in warning state.
--hy-switch-foreground-danger-restvar(--hy-foreground-danger-rest)On track, thumb icon and message in error state.
--hy-switch-foreground-warning-restvar(--hy-foreground-warning-rest)On track, thumb icon and message in warning state.
--hy-switch-gap-layout-2xsvar(--hy-gap-layout-2xs)Gap between the message icon and its text.
--hy-switch-handle-indicator-size-smvar(--hy-indicator-size-sm)Thumb size for the small size variant; the track derives from the thumb, so this scales the whole control.
--hy-switch-handle-indicator-size-mdvar(--hy-indicator-size-md)Thumb size for the medium size variant.
--hy-switch-handle-indicator-size-lgvar(--hy-indicator-size-lg)Thumb size for the large size variant.
--hy-switch-container-indicator-size-smvar(--hy-indicator-size-sm)Row floor (min-height) for the small size variant. The row is sized to its content: by default the floor is the size's indicator-size rung, so stacked switches and their top-label / helper text stay tight. Track and handle ride the size's --hy-switch-handle-* rung. To line a switch up with an input in a form row, set this to a control height (e.g. var(--hy-control-height-sm)) per instance or per design system. Coarse pointers keep a touch-target floor.
--hy-switch-container-indicator-size-mdvar(--hy-indicator-size-md)Row floor (min-height) for the medium size variant.
--hy-switch-container-indicator-size-lgvar(--hy-indicator-size-lg)Row floor (min-height) for the large size variant.
--hy-switch-gap-form-smvar(--hy-gap-form-sm)The only spacing between the top-label, the row and the helper text or message.
--hy-switch-motion-transition-hover-durationvar(--hy-motion-transition-hover-duration)Transition duration for hover and focus state feedback
--hy-switch-motion-transition-hover-easingvar(--hy-motion-transition-hover-easing)Transition easing for hover and focus state feedback

Methods ​

checkValidity() ​

Checks if the switch is valid according to form validation rules. Returns true if valid, false otherwise.

javascript
const switchEl = document.querySelector('hy-switch');
if (!switchEl.checkValidity()) {
  console.log('Switch is invalid');
}

reportValidity() ​

Checks validity and shows browser validation UI if invalid. Returns true if valid, false otherwise.

javascript
const switchEl = document.querySelector('hy-switch');
if (!switchEl.reportValidity()) {
  console.log('Switch is invalid and message shown');
}

toggle() ​

Toggles the switch between 'on' and 'off' states programmatically. Does nothing if the switch is disabled.

javascript
const switchEl = document.querySelector('hy-switch');
switchEl.toggle(); // off -> on or on -> off

turnOn() ​

Turns the switch to 'on' state programmatically. Does nothing if the switch is disabled.

javascript
const switchEl = document.querySelector('hy-switch');
switchEl.turnOn();

turnOff() ​

Turns the switch to 'off' state programmatically. Does nothing if the switch is disabled.

javascript
const switchEl = document.querySelector('hy-switch');
switchEl.turnOff();

focus() ​

Sets focus on the switch input.

javascript
const switchEl = document.querySelector('hy-switch');
switchEl.focus();

setTabIndex() ​

Projects a tabindex onto the inner input — called by a parent roving-tabindex controller (hy-toolbar) so the switch joins its single tab stop instead of staying one of its own.

blur() ​

Removes focus from the switch input.

javascript
const switchEl = document.querySelector('hy-switch');
switchEl.blur();

Built with Lit. Documented with VitePress.