Skip to content

Switch — the cross-framework headless toggle

Switch is a headless, fully-accessible on/off toggle with no third-party engine behind it. It covers the whole behaviour surface: a boolean two-way value, toggling on click and on Space/Enter, role="switch" with aria-checked / aria-disabled / aria-readonly, focus management, and the disabled / readonly states. The same component ships for React, Vue, Svelte, Angular, Solid, and Lit.

The foundation is the platform itself: a focusable native element, a native click, and a Space/Enter keydown. The on/off state is modelValue (the sole model: true prop), typed boolean; there is no draft local state, so the thumb position and aria-checked derive straight from the bound value. Rozie owns the author-side API: the two-way r-model:modelValue, the toggle choreography, the ARIA wiring, and the token-themed skin.

Every visual value is a CSS custom property, so the switch re-skins to any design system, with ready-made bridges for shadcn/ui, Material 3, and Bootstrap 5.

The @rozie-ui/switch packages

Switch ships as six pre-compiled, per-framework packages. Install the one for your framework; there is no build step and no Rozie toolchain to set up:

PackageInstallREADME
@rozie-ui/switch-reactnpm i @rozie-ui/switch-reactreact/README
@rozie-ui/switch-vuenpm i @rozie-ui/switch-vuevue/README
@rozie-ui/switch-sveltenpm i @rozie-ui/switch-sveltesvelte/README
@rozie-ui/switch-angularnpm i @rozie-ui/switch-angularangular/README
@rozie-ui/switch-solidnpm i @rozie-ui/switch-solidsolid/README
@rozie-ui/switch-litnpm i @rozie-ui/switch-litlit/README

Each package carries only its framework peer (react + react-dom, vue, svelte, @angular/core + @angular/common + @angular/forms, solid-js, or lit + @lit-labs/preact-signals + @preact/signals-core).

Quick start

Two-way bind modelValue and (optionally) set an ariaLabel. The switch toggles on click and on Space/Enter; @change fires on every committed change:

rozie
<components>
{
  Switch: './Switch.rozie',
}
</components>

<data>
{
  wifi: false,
}
</data>

<template>
  <Switch r-model:modelValue="$data.wifi" ariaLabel="Wi-Fi" @change="onChange" />
</template>

r-model:modelValue is Rozie's two-way bind: the consumer hands Switch a boolean, Switch writes the new state back on every toggle, and the framework reconciler picks it up with no onChange → setState wiring. Because modelValue is the component's sole model: true prop, the Angular output additionally implements ControlValueAccessor, so a Switch is a form control ([formControl] / [(ngModel)] bind directly).

API

Props

NameTypeDefaultRuntime-updatable?Description
modelValueBooleanfalseyes (via r-model)The on/off state — the sole model: true prop, so Angular emits a ControlValueAccessor. true is checked/on; reflected as aria-checked.
disabledBooleanfalseyesDisable the control entirely — non-focusable, non-toggleable, aria-disabled set. Also sets the Angular CVA disabled state.
readonlyBooleanfalseyesShow + focus the state but block toggling (click and keyboard are ignored). Reflected as aria-readonly.
ariaLabelStringnullyesAccessible name applied to the role="switch" control (aria-label).

Events

EventDescription
changeFired whenever the switch is toggled — by a click, by Space/Enter, or by the programmatic toggle() handle. Payload { checked } — the new boolean state. (No-op while disabled or readonly.)

Imperative handle

Declared once in the source via $expose; obtained through each framework's native ref mechanism.

MethodDescription
focusMove DOM focus to the switch control. Deliberately named focus, overriding the inherited HTMLElement.focus on the Lit custom element; the override is intentional, and the compiler accepts it with a warning. This mirrors the otp / number-field precedent.
toggleFlip the on/off state (same funnel as a click / Space / Enter) and emit change. A no-op while disabled or readonly.

Slots

SlotParams
(default)checked, toggle

The default slot is scoped — it receives { checked, toggle } so you can render a fully custom thumb/track (or a label + icon) while keeping the accessible button, keyboard, and two-way binding. Omit it and the component renders its built-in tokenised track + thumb.

Theming

Every cosmetic value the component renders is a --rozie-switch-* CSS custom property with a built-in fallback, so it works with zero configuration and remains fully re-skinnable. Override tokens at any ancestor scope:

css
.rozie-switch {
  --rozie-switch-on-bg: #16a34a;
  --rozie-switch-width: 2.75rem;
  --rozie-switch-height: 1.5rem;
  --rozie-switch-thumb-bg: #fff;
}

Only cosmetic values flow through tokens; the structural rules (the focusable button, the track/thumb box model, the checked-state transform) compile per-leaf and are not consumer-overridable.

The complete token table and the design-system bridges live on the dedicated theming page.

Accessibility

The control is a native <button> with role="switch" and aria-checked reflecting modelValue (never dropped on false). It carries aria-disabled / aria-readonly for those states, and aria-label from ariaLabel. It is keyboard-operable per the WAI-ARIA switch pattern — Space and Enter both toggle it — and tabindex is 0 when interactive, dropped when disabled. Provide ariaLabel (or wire an external <label>) so the switch is announced.

Pre-1.0 — APIs may change between minor versions.