Switch
Switch represents a single setting that turns on or off the moment someone
toggles it, with no separate save step. It shares its form participation,
validation, and keyboard behavior with
Dependencies
When to use
Use a switch for a setting that takes effect the moment it's toggled, with no "Save" button needed, such as enabling notifications or switching to dark mode. See Alternatives below for a better fit when the choice only applies once the whole form is submitted, or when it's really about picking one option out of several.
Install & usage
Pick a framework in the toolbar above and these snippets adapt.
npm install @eunomia/elements
import "@eunomia/elements/switch.js";
<eun-switch name="notifications" value="enabled">
Enable notifications
</eun-switch>
Importing the file registers <eun-switch> as a custom element, with no
further setup needed. It works with any framework, or none, since it's a
standard web component.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/switch.js";
</script>
<eun-switch name="notifications" value="enabled">
Enable notifications
</eun-switch>
npm install @eunomia/elements
import "@eunomia/elements/switch.js";
function NotificationsField() {
return (
<eun-switch name="notifications" value="enabled">
Enable notifications
</eun-switch>
);
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/switch.js";
export function NotificationsField() {
return (
<eun-switch name="notifications" value="enabled">
Enable notifications
</eun-switch>
);
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/switch.js";
</script>
<template>
<eun-switch name="notifications" value="enabled">
Enable notifications
</eun-switch>
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/switch.js";
@Component({
selector: "app-notifications-field",
template: `
<eun-switch name="notifications" value="enabled">
Enable notifications
</eun-switch>
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class NotificationsFieldComponent {}
Alternatives
eun-checkboxeun-radioGuidance
- Use a switch for a setting that takes effect immediately when toggled (e.g. "Enable notifications", "Dark mode")
- Label it with the setting it controls, not its current state: "Notifications", not "Notifications are on" (see the Accessibility tab)
- Pair a
requiredswitch with a visible note at the top of the form, not just the error message alone
- Using a switch for a choice that only applies once the form is submitted, or to pick one option among several: see Alternatives above
- Hiding the
hintin a tooltip instead of rendering it under the field - Overriding colors with inline styles instead of the
--switch-*CSS variables
Live testing
Properties
Switch <eun-switch>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| default-checked | boolean | — | The checked state applied when the switch connects, and restored on form reset |
| hide-icon | boolean | false | Whether the icon inside the thumb is hidden |
| hide-error | boolean | false | Whether the error message is hidden |
| default-value | string | — | Has no effect on the switch. A form reset restores the checked state from defaultChecked instead |
| label | string | — | The label to display when not using the default slot |
Import the exact TypeScript type behind any property above, see
Properties
JS-only — no matching HTML attribute, set these from a script or a template binding.
| Name | Type | Default | Description |
|---|---|---|---|
| checked | boolean | false | Whether the switch is on |
| defaultChecked | boolean | — | The checked state applied when the switch connects, and restored on form reset |
| disabled | boolean | false | Whether the switch is disabled |
| readonly | boolean | false | Whether the switch is read only |
| hideError | boolean | false | Whether the error message is hidden |
| required | boolean | false | Whether the switch must be turned on to be valid |
| name | string | — | The name of the switch field |
| value | string | — | The value submitted with the switch |
| hint | string | — | A short message displayed under the switch |
| validators | Array<Validators<boolean>> | — | A list of validation rules applied to the checked state |
Slots
| Name | Description |
|---|---|
| (default) | The switch label |
| icon-on | Replaces the default icon shown in the thumb while checked. Ignored when hideIcon is set |
| icon-off | Replaces the default icon shown in the thumb while unchecked. Ignored when hideIcon is set |
Events
| Name | Type | Description |
|---|---|---|
| click | Fired when the switch is clicked | |
| eunchange | ChangeEvent | Fired when the switch's checked state changes |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --switch-track-width | Sets the width of the track |
| --switch-track-height | Sets the height of the track |
| --switch-track-background | Sets the track color while off |
| --switch-track-background-checked | Sets the track color while on |
| --switch-track-background-hover | Sets the track color on hover while off |
| --switch-track-background-pressed | Sets the track color while pressed and off |
| --switch-thumb-size | Sets the width and height of the thumb, independently of the track height |
| --switch-thumb-background | Sets the color of the thumb |
| --switch-thumb-background-checked | Sets the color of the thumb while checked, falling back to switch-thumb-background when unset |
| --switch-icon-color | Sets the color of the icon inside the thumb |
| --switch-icon-color-checked | Sets the color of the icon while checked, falling back to switch-icon-color when unset |
| --switch-error-color | Sets the color of the error message |
| --switch-hint-color | Sets the color of the hint message |
| --switch-focus-outline-color | Sets the color of the focus outline |
Basic
<eun-switch name="notifications" value="enabled">
Enable notifications
</eun-switch>
States
Checked
Set checked to render the switch already on.
<eun-switch name="notifications" value="enabled" checked>
Enable notifications
</eun-switch>
Disabled
Disabled switches remain visible but can't be toggled, and are excluded from
the tab order and from form submission. A disabled switch always renders as
unchecked, regardless of its checked property.
<eun-switch disabled>Disabled</eun-switch>
Readonly
A readonly switch looks active, stays focusable, and its value still
submits, but it can't be toggled by the user. Unlike disabled, it still
communicates its current state instead of looking inert.
<eun-switch readonly>Readonly, off</eun-switch>
<eun-switch readonly checked>Readonly, on</eun-switch>
Required
A required switch must be toggled on to be considered valid.
<eun-switch required>Accept marketing emails</eun-switch>
With hint
hint renders a short message under the switch, replaced by the error
message once the switch becomes invalid (see Error below).
<eun-switch hint="You can change this anytime in settings">
Enable notifications
</eun-switch>
Error
If a hint is defined, it shares the same space as the error message, and
the error message always takes priority.
<eun-switch required hint="Enable this setting to continue">
Enable notifications
</eun-switch>
document.querySelector("eun-switch").validators = [
{
isValid: (checked: boolean) => checked,
message: "Enable this setting to continue",
},
];
Forms
Like every other Eunomia field, eun-switch is a real
<form>'s
submission and reset. Two things about reset specifically catch people out,
since a switch's reset semantics follow checked, not value, unlike a
plain text field:
default-checkedis what a reset restores, notdefault-value.default-valueexists oneun-switch(inherited from the same base as every other field) but has no effect here:valueis a fixed submission string (value="enabled"), not something the user edits, so there's nothing fordefault-valueto meaningfully "restore" it to. Onlydefault-checked(or plainchecked, on connection) decides what a reset reverts to. Set it explicitly if the switch should come back on, since it otherwise defaults tofalse.- Resetting doesn't fire
eunchange. Exactly like a native<input>,form.reset()changescheckedwithout dispatching a change event. Listen forreseton the<form>itself if you need to react to it (see the demo below).
{}
<form id="my-form">
<eun-switch name="notifications" value="enabled" default-checked>
Enable notifications
</eun-switch>
<eun-switch name="newsletter" value="subscribed">
Subscribe to newsletter
</eun-switch>
<button type="submit">Submit</button>
<button type="reset">Reset</button>
</form>
const form = document.querySelector("#my-form");
// Fires on every toggle, not on reset.
form.addEventListener("eunchange", () => {
console.log(Object.fromEntries(new FormData(form)));
});
// Fires on reset (native form event), not on eunchange. `setTimeout` lets
// the switches' own reactive update settle first, same as any other
// property change reflected asynchronously.
form.addEventListener("reset", () => {
setTimeout(() => console.log(Object.fromEntries(new FormData(form))));
});
"Notifications" comes back on after Reset (from its default-checked), while "Newsletter" comes back off, since it never had one.
Icons
By default, the thumb shows a check icon while on and a close icon while
off, rendered through eun-icon (imported separately, see the dependency
badges above). Skip that import if every switch on the page sets
hide-icon, or slots its own icon markup into icon-on/icon-off instead.
The icons are purely decorative either way, since the state itself is
already announced through aria-checked (see the Accessibility tab).
Set hide-icon to remove the icon entirely, or slot your own into
icon-on/icon-off. Either way, the icon stays visible while readonly
too, since that's specifically when a state cue that doesn't depend on
toggling still matters most.
<eun-switch checked>Default icons, on</eun-switch>
<eun-switch>Default icons, off</eun-switch>
<eun-switch hide-icon checked>Icon hidden</eun-switch>
Custom icons
icon-on/icon-off override the default glyph for their respective state.
hide-icon still takes priority over either, removing the whole icon area
whether it's the default glyph or a custom one.
<eun-switch checked>
Do not disturb
<span slot="icon-on"><eun-icon name="bedtime" size="12px"></eun-icon></span>
<span slot="icon-off"><eun-icon name="wb_sunny" size="12px"></eun-icon></span>
</eun-switch>
Custom
Override the --switch-* CSS variables, listed in full in the API tab.
<eun-switch class="custom-switch" checked>Custom switch</eun-switch>
.custom-switch {
--switch-track-background-checked: #be185d;
--switch-thumb-background: #fff;
}
Thumb size and icon color
--switch-thumb-size is independent from --switch-track-height, so sizing
the thumb doesn't require resizing the whole track to match. The
-checked variants (--switch-thumb-background-checked,
--switch-icon-color-checked) only apply while on, falling back to the
plain (off) variable otherwise, so setting just one still gives a
consistent look for both states.
<eun-switch class="custom-thumb" checked>Custom thumb</eun-switch>
.custom-thumb {
--switch-thumb-size: 26px;
--switch-thumb-background-checked: #fde68a;
--switch-icon-color-checked: #92400e;
}
Keyboard interactions
| Key | Action |
|---|---|
Tab / Shift+Tab |
Moves focus in/out of the switch (single tab stop) |
Space |
Toggles checked |
Aria attributes
eun-switch doesn't wrap a native <input>. The host element itself is the
control, so every ARIA state lives directly on it:
role="switch", set on connect if not already present on the host (rather thancheckbox's own default), since the action takes effect immediately instead of waiting on a later form submissionaria-checked="true"|"false": reflectscheckedaria-disabled/aria-readonly: kept in sync withdisabled/readonlyaria-required: kept in sync withrequiredaria-invalid: kept in sync withvalid, oncedisabledorhideErrorare also accounted foraria-description: set fromhint/error content while present (via the inheriteddescription), removed otherwise
Label accessibility
role="switch" requires an author-supplied accessible name, the same
requirement checkbox's own role="checkbox" carries. eun-switch gets it
from its slotted text content (<eun-switch>Enable notifications</eun-switch>,
used throughout this page): the slotted text is what ends up read as the
accessible name in every browser tested, relying on content-based name
computation. Both the label and the track are clickable, extending the
interactive area.
Label the switch with the setting it controls, not its current state.
Use "Notifications", not "Notifications are on": the state itself is
already announced separately via aria-checked, so baking it into the
label would duplicate, and eventually contradict, that announcement once
toggled.
Disabled vs. readonly
disabled removes the switch from the tab order and excludes its value from
form submission entirely: it also forces the switch to render unchecked
regardless of its actual checked value, since a disabled control
communicating a hidden checked state serves nobody. readonly keeps it
focusable and its value submitted, and additionally prevents toggling,
communicating "you can't change this right now", not "this doesn't apply".
A disabled switch may cause usability and accessibility issues for people
with disabilities relying on that state to still be inspectable. Prefer
readonly whenever the value still needs to stay perceivable.
Icon
The check/close icon inside the thumb (see hide-icon and
icon-on/icon-off in the Examples tab) is purely decorative: aria-hidden
regardless of whether it's the default glyph or a slotted custom one, since
the state it mirrors is already conveyed through aria-checked. It's the
one visual this component keeps unmuted while readonly, on purpose.
readonly already mutes the track's own color the same way disabled does
(see above), so without the icon, a readonly switch's on/off state would
only be legible through color contrast, not through a shape difference.
Required field
required means "must be checked", validated against the switch's actual
checked state, not against its value attribute, which is typically a
fixed string (e.g. value="enabled") present whether or not the switch is
on, and would otherwise never look "empty" to a generic required check.
Add a visible note near the top of the form in addition to relying on the
error message alone, and prefer hint to explain why toggling it on is
required, not just that it is.
Screen reader restitution
By default, screen readers read out the name, on/off state, required state, and type (switch). The order may vary depending on the screen reader and its configuration. The disabled state is rendered differently depending on the screen reader:
- VoiceOver (macOS/iOS): grayed out
- NVDA and JAWS: unavailable
- Narrator and TalkBack: disabled
Reference links