HySwitch
Usage examples on this page are written for Lit / plain HTML (
<hy-switch>). The same component ships asHySwitchin@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
reasonfield ('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, neveraria-invalidNeither touches form validity — that comes fromrequired.
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-checkboxin the form, recorded on submit. - More than two options, or switches that would exclude each other:
hy-radio-grouporhy-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
<hy-switch name="notifications" value="enabled" label="Enable notifications"> </hy-switch>Form submission
<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
<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
<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
<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
<hy-switch
name="auto-delete"
label="Delete files after 30 days"
state="on"
warning-message="Deleted files cannot be recovered"
>
</hy-switch>Different sizes
<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
<hy-switch label="Disabled off" disabled></hy-switch>
<hy-switch label="Disabled on" state="on" disabled></hy-switch>Programmatic control
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
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
<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
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
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
state | state | 'off' | 'on' | 'off' | The current state of the switch ('off' or 'on'). |
label | label | string | '' | The text label displayed next to the switch. |
iconOn | icon-on | string | '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. |
iconOff | icon-off | string | '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. |
helperText | helper-text | string | '' | Helper text displayed below the switch. An error or warning message replaces it. |
topLabel | top-label | string | '' | Top label displayed above the switch, useful for grouping or section headers. |
disabled | disabled | boolean | false | Disables the switch, making it non-interactive. |
required | required | boolean | false | Makes the switch required for form submission (must be in 'on' state). |
name | name | string | '' | The name of the switch for form submission. |
value | value | string | 'on' | The value submitted with the form when the switch is 'on'. |
error | error | boolean | false | Error 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. |
errorMessage | error-message | string | '' | Error message shown under the switch (replaces the helper text). Setting it raises error, clearing it lowers it. |
warning | warning | boolean | false | Warning 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. |
warningMessage | warning-message | string | '' | Warning message shown under the switch (replaces the helper text). Setting it raises warning. |
size | size | 'small' | 'medium' | 'large' | 'medium' | The size of the switch ('small', 'medium', or 'large'). |
layout | layout | '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. |
labelPlacement | label-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. |
ariaDescribedby | aria-describedby | string | '' | Additional element IDs to include in aria-describedby for accessibility. |
switchId | switch-id | string | '' | Id for the switch input element; empty uses the fixed input (components.md "Ids"). |
helperTextId | helper-text-id | string | '' | Id for the helper text element; empty uses the fixed helper. |
topLabelId | top-label-id | string | '' | Id for the top label element; empty uses the fixed top-label. |
autofocus | autofocus | boolean | false | Marks 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. |
checked | checked | boolean | — | 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
| Event | Detail | Description |
|---|---|---|
change | — | Fired when the state changes (the form's change event); detail: state, checked, value and reason |
input | — | Standard form input event |
CSS Parts
| Part | Description |
|---|---|
base | The switch's base wrapper element |
helper-text | The helper text element |
error-message | The error message element |
warning-message | The warning message element |
message-icon | The 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.
| Property | Default | Description |
|---|---|---|
--hy-switch-text-transform | none | Text transform for switch label |
--hy-switch-thumb-icon-on-display | flex | Whether 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-display | none | Whether 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-1 | var(--hy-shadow-elevation-1) | Box shadow for the switch thumb at rest |
--hy-switch-shadow-elevation-2 | var(--hy-shadow-elevation-2) | Box shadow for the switch thumb on hover |
--hy-switch-label-md-font-family | var(--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-light | var(--hy-background-danger-tint-light) | Off track in error state. |
--hy-switch-background-warning-tint-light | var(--hy-background-warning-tint-light) | Off track in warning state. |
--hy-switch-foreground-danger-rest | var(--hy-foreground-danger-rest) | On track, thumb icon and message in error state. |
--hy-switch-foreground-warning-rest | var(--hy-foreground-warning-rest) | On track, thumb icon and message in warning state. |
--hy-switch-gap-layout-2xs | var(--hy-gap-layout-2xs) | Gap between the message icon and its text. |
--hy-switch-handle-indicator-size-sm | var(--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-md | var(--hy-indicator-size-md) | Thumb size for the medium size variant. |
--hy-switch-handle-indicator-size-lg | var(--hy-indicator-size-lg) | Thumb size for the large size variant. |
--hy-switch-container-indicator-size-sm | var(--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-md | var(--hy-indicator-size-md) | Row floor (min-height) for the medium size variant. |
--hy-switch-container-indicator-size-lg | var(--hy-indicator-size-lg) | Row floor (min-height) for the large size variant. |
--hy-switch-gap-form-sm | var(--hy-gap-form-sm) | The only spacing between the top-label, the row and the helper text or message. |
--hy-switch-motion-transition-hover-duration | var(--hy-motion-transition-hover-duration) | Transition duration for hover and focus state feedback |
--hy-switch-motion-transition-hover-easing | var(--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.
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.
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.
const switchEl = document.querySelector('hy-switch');
switchEl.toggle(); // off -> on or on -> offturnOn()
Turns the switch to 'on' state programmatically. Does nothing if the switch is disabled.
const switchEl = document.querySelector('hy-switch');
switchEl.turnOn();turnOff()
Turns the switch to 'off' state programmatically. Does nothing if the switch is disabled.
const switchEl = document.querySelector('hy-switch');
switchEl.turnOff();focus()
Sets focus on the switch input.
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.
const switchEl = document.querySelector('hy-switch');
switchEl.blur();