Appearance
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:
| Package | Install | README |
|---|---|---|
@rozie-ui/switch-react | npm i @rozie-ui/switch-react | react/README |
@rozie-ui/switch-vue | npm i @rozie-ui/switch-vue | vue/README |
@rozie-ui/switch-svelte | npm i @rozie-ui/switch-svelte | svelte/README |
@rozie-ui/switch-angular | npm i @rozie-ui/switch-angular | angular/README |
@rozie-ui/switch-solid | npm i @rozie-ui/switch-solid | solid/README |
@rozie-ui/switch-lit | npm i @rozie-ui/switch-lit | lit/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
| Name | Type | Default | Runtime-updatable? | Description |
|---|---|---|---|---|
modelValue | Boolean | false | yes (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. |
disabled | Boolean | false | yes | Disable the control entirely — non-focusable, non-toggleable, aria-disabled set. Also sets the Angular CVA disabled state. |
readonly | Boolean | false | yes | Show + focus the state but block toggling (click and keyboard are ignored). Reflected as aria-readonly. |
ariaLabel | String | null | yes | Accessible name applied to the role="switch" control (aria-label). |
Events
| Event | Description |
|---|---|
change | Fired 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.
| Method | Description |
|---|---|
focus | Move 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. |
toggle | Flip the on/off state (same funnel as a click / Space / Enter) and emit change. A no-op while disabled or readonly. |
Slots
| Slot | Params |
|---|---|
(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.