Toggle Switch

Free

Accessible toggle for boolean settings in multiple sizes, colors and label positions.

</>sv-toggle-switch toggleswitchboolean
import { SvToggleSwitchComponent } from '@styloviz/toggle-switch';

Demo & usage

Basic usage

Live, interactive instance.

More examples

Sizes & colors

example.htmlAngular
<sv-toggle-switch label="Small" size="sm" [(checked)]="a" />
<sv-toggle-switch label="Success" variant="success" [(checked)]="b" />

On/off labels inside the track

example.htmlAngular
<sv-toggle-switch onLabel="On" offLabel="Off" [(checked)]="enabled" />

Reactive forms + required

Works with formControlName via ControlValueAccessor; required sets aria-required and the description is linked for assistive tech.

example.htmlAngular
<sv-toggle-switch
  formControlName="acceptTerms"
  label="I accept the terms"
  description="You must accept to continue."
  [required]="true" />

A setting that has to reach a server

A switch that silently flips back three seconds later is the defining trust failure of a settings screen. The switch moves first — waiting for the round-trip reads as broken — so it owes the host a way back. `(toggleRequest)` carries an idempotency key and a guarded `rollback()`, and `[pending]` is the lock that makes the rollback unambiguous: with two writes outstanding there is no single previous state to return to. `(toggled)` still fires for hosts that need neither.

example.htmlAngular
<sv-toggle-switch
  label="Two-factor authentication"
  [checked]="twoFactor()"
  [pending]="saving()"
  [error]="failure()"
  (toggleRequest)="save($event)" />

save(e: SvToggleChange): void {
  this.saving.set(true);
  this.api.setTwoFactor(e.checked, e.requestKey).subscribe({
    next:  () => this.saving.set(false),
    error: () => { e.rollback(); this.failure.set('Could not save that.'); this.saving.set(false); },
  });
}

Pending is not loading

They wear the same spinner and mean opposite things. `loading` says *we do not know what this switch is* — a settings screen that rendered before its settings arrived, so dimming it is honest. `pending` says *we know exactly what the user asked for, it is already drawn, and the server has not agreed yet* — so it spins without dimming, because fading a value the user just chose reads as "this did not take".

example.htmlAngular
<!-- The value has not arrived. Nothing on screen is trustworthy yet. -->
<sv-toggle-switch label="Email digest" [loading]="settings.isLoading()" />

<!-- The value is the user's own choice, and the write is out. -->
<sv-toggle-switch label="Email digest" [checked]="digest()" [pending]="saving()" />

A lock that says why

A disabled switch tells the user "not you, not now" and nothing else, leaving them to guess between "not on your plan", "your admin turned this off" and "this is broken" — three different next actions, and only the host knows which applies. `lockReason` locks the control, renders the reason and points `aria-describedby` at it, and the switch stays in the tab order so the readers most likely to need the explanation can reach it.

example.htmlAngular
<sv-toggle-switch
  label="Single sign-on"
  [checked]="true"
  lockReason="Enforced by your organization" />

When to use

  • Toggling a single setting that takes effect immediately (no Save needed).
  • The choice is binary and the default is meaningful — e.g. notifications on/off.
  • Use a Checkbox instead whenever the change only lands on submit — a form field, a terms acceptance, a row selection, or anything a Cancel button is expected to undo. A switch promises "this is on now", so putting one inside a form with a Save button makes a promise the form then breaks; and if the user closes the dialog, nothing tells them which of the two things they saw was true.
  • Use a Checkbox instead when the action needs confirming. A switch cannot ask "are you sure" without contradicting itself: it has already moved by the time the dialog opens, and a cancelled confirmation leaves it to move back on its own — which is indistinguishable from a failed save.

Async states

  • loading
  • error

Server writes

Writes
  • toggleRequest

Each payload carries a requestKey — send it with the request so a retry cannot apply the same change twice.

Rollback
Call event.rollback() when the save fails — the change is already on screen, and this is what puts it back.
While saving
Bind pending — with two writes outstanding there is no single state to roll back to, so the action stays locked until the first one lands.
On failure
Pass the failure to error to show it in place — without taking away what the user was working on.

Inputs

NameTypeDefaultDescription
checkedboolean (two-way)falseCurrent on/off state. Use `[(checked)]` for two-way binding.
labelstring''Visible label text.
descriptionstring''Smaller descriptor line beneath the label.
labelPositionToggleLabelPosition'right'Position of the label relative to the track.
onLabelstring''Text shown inside the track when ON. Keep ≤ 3 characters for `md` size, fewer for smaller sizes.
offLabelstring''Text shown inside the track when OFF.
onIconstring''Icon-library NAME rendered inside the thumb when the toggle is ON — e.g. `'check'`. Omit to show a plain thumb. BREAKING (icon sweep): was `onIconPath`, a raw SVG `d` string.
offIconstring''Icon-library NAME rendered inside the thumb when the toggle is OFF — e.g. `'close'`. Was `offIconPath`.
sizeToggleSize'md'Size of the toggle track and thumb.
variantToggleVariant'primary'Active-state color variant.
disabledbooleanfalseDisable interaction.
requiredbooleanfalseMark the control as required (sets `aria-required`).
loadingbooleanfalseThe switch's own value is not known yet — a settings screen that rendered before its settings arrived. Shows a spinner in the thumb and blocks interaction.
pendingbooleanfalseThe user's change is applied on screen and the write is in flight. Why this is not `loading`: They are opposite states wearing the same spinner. `loading` means *we do not know what this switch is* — there is nothing on screen to trust yet. `pending` means *we know exactly what the user asked for, it is already drawn, and the server has not agreed yet*. Collapsing the two costs the host the only lock that makes `rollback()` unambiguous: with two writes outstanding there is no single previous state to return to, so the older handle would silently discard the newer change. Bind it for the whole round-trip and clear it in both branches.
errorSvDataError | string | nullnullThe write failed. Why this is not `serverError`: They are different facts about different things, and using one for the other is a real bug rather than a naming preference. `serverError` is the field-error contract: *the server rejected this VALUE*, and it self-expires on the next change because a rejection stops describing a value the moment the value moves. A failed write is *the server never recorded the change* — and reverting it **is** a change, so routing it through `serverError` would clear the message in the same tick the rollback ran. The user would see the switch move back and be told nothing, which is the precise failure this whole contract exists to prevent. Accepts an `SvDataError` or a plain string. Roll the value back as well — a message beside a switch still showing the state that failed to save is worse than either on its own.
lockReasonstring''Why this switch cannot be changed. Non-empty locks the control and renders the reason, which `aria-describedby` also points at. Distinct from `disabled`, deliberately. A disabled switch is a dead end: it tells the user they may not, and nothing else, so they are left to guess between "not on your plan", "your admin turned this off" and "this is broken". Those need different next actions, and only the host knows which one applies.
ariaLabelstring''`aria-label` override — required when `labelPosition === 'hidden'` so screen readers still announce the control's purpose.
inputIdstring''id forwarded to the hidden `<input>` for `<label for="">` use.
customClassstring''Additional CSS classes on the root wrapper element.
errorMessagestring''Client-side validation message.
serverErrorSvServerErrornullA rejection reported by the server about this value. Self-expiring.

Outputs

EventPayload typeDescription
toggledbooleanEmits the new boolean state after every toggle.
toggleRequestSvToggleChangeThe same toggle, carrying what a server write needs: an idempotency key and a guarded way back. Additive, not a replacement — `toggled` still fires, and a switch that only writes to local state has no use for either of these. Both are emitted for one interaction, in this order.
checkedChangebooleanEmits the new `checked` whenever it changes — the output half of `[(checked)]`.

Customization & theming

  • Set `size` (xs, sm, md, lg) and `variant` (primary, success, warning, danger, neutral) for the active color.
  • Place the label with `labelPosition` (right, left, top, hidden), add a `description` line and inline `onLabel` / `offLabel` track text.
  • Show inline `onIcon` / `offIcon` icons inside the thumb; toggle `disabled`, `loading` and `required`.
  • Two-way bind with `[(checked)]` or bind a reactive control; listen to `(toggled)` for the new boolean, or `(toggleRequest)` for a `{ checked, requestKey, rollback }` payload when a server has to agree.
  • `pending` locks the switch while a write is in flight without dimming it; `lockReason` locks it and says why.

Best practices

  • Do: Phrase the `label` as the enabled state ("Email notifications"), not a question.
  • Do: Apply the change instantly on toggle, and bind `[pending]` — not `[loading]` — while it persists. `loading` is for a value you do not have yet; `pending` is for one the user just chose.
  • Do: Handle `(toggleRequest)` for anything that reaches a server: send its `requestKey` so a retried request cannot land twice, and call its `rollback()` on failure so the switch does not sit there lying.
  • Do: Report the failure as well as reverting. A switch that moves back on its own, with nothing said, is the exact experience this contract exists to prevent — bind `[error]`. Not `[serverError]`: that one self-expires on the next value change, and a rollback IS a value change, so the message would clear in the same tick it appeared.
  • Don't: Don't use a toggle when the user must explicitly submit — that's a checkbox's job.
  • Don't: Don't reach for `[disabled]` when you know why it is locked. `lockReason` says so, and keeps the switch focusable so the explanation is reachable.
  • Tip: Set `ariaLabel` when there is no visible `label`.

Accessibility

  • Uses `role="switch"` with `aria-checked`; operable with Space/Enter and a visible focus ring.
  • The visible `label` is associated via `<label for>`, so clicking the text toggles the switch; a unique id is generated automatically when `inputId` is omitted.
  • The accessible name comes from `aria-labelledby` (visible label) or `ariaLabel` (hidden); `description` is linked with `aria-describedby`.
  • `required` sets `aria-required`; `loading` and `pending` both set `aria-busy`; `disabled`, `pending`, `lockReason` and a programmatic `formControl.disable()` all set `aria-disabled` and block interaction.
  • A `lockReason` is rendered as text and linked with `aria-describedby`, and the switch keeps `tabindex="0"` — a lock explained only in a tooltip, or on an unfocusable control, is unreachable by the people most likely to need it.

Feedback on Toggle Switch

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.