Primitives Forms
Switch
A native checkbox with role="switch". The role changes what assistive technology announces; the checked state, keyboard handling and form participation stay native, and the package ships no runtime. Tier 0.
- Tier
- 0 · Native
- Behavior JS
- 0 B brotli · 0 B gzip ·
fixtures/· methodresults.json (native-only) - Platform features
<input type="checkbox">,role="switch",:checked- Shared listeners
- none
- Per-instance listeners
- none
- Lazy state
- none
- Native base
<input type="checkbox" role="switch">
On this page
Example
<label>
<input type="checkbox" role="switch" name="notifications" data-slean="switch" />
Email notifications
</label>
<label>
<input type="checkbox" role="switch" name="digest" data-slean="switch" checked />
Weekly digest
</label>
<label>
<input type="checkbox" role="switch" name="preview" data-slean="switch" disabled />
Preview features
</label><script lang="ts">
// Nothing to import: the browser owns the checkbox. The stylesheet is optional.
import '@svelte-lean/styles/switch.css';
</script>
<label>
<input type="checkbox" role="switch" name="notifications" data-slean="switch" />
Email notifications
</label>
<label>
<input type="checkbox" role="switch" name="digest" data-slean="switch" checked />
Weekly digest
</label>
<label>
<input type="checkbox" role="switch" name="preview" data-slean="switch" disabled />
Preview features
</label>Why this implementation exists
A switch is a checkbox whose change takes effect at once, and assistive technology announces it
as "on" or "off" rather than "checked". ARIA 1.2 defines role="switch" for exactly
that, and current screen readers map it on a native checkbox. Nothing else differs: Space
toggles, the label names it, the form submits it, :checked styles it. Svelte Lean
therefore ships a stylesheet that draws a track and a thumb, a contract that says when a switch
is the right control, and no runtime, no aria-checked mirror and no click handler.
The browser owns
- the checked and disabled state
- Space to toggle and the label click
- form participation and the change event
- the switch role announcement (ARIA 1.2)
Svelte Lean owns
- switch.css: track, thumb, motion tokens, the coarse-pointer hit area
- the contract
- documentation
Usage
The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.
npm install @svelte-lean/stylespnpm add @svelte-lean/stylesyarn add @svelte-lean/stylesbun add @svelte-lean/stylesimport '@svelte-lean/styles/tokens.css';
import '@svelte-lean/styles/base.css';
import '@svelte-lean/styles/switch.css';Use a switch when toggling has an immediate effect: a setting that applies as soon as it is
flipped. Use a Checkbox for a value submitted with a form. Do
not use the WebKit-only switch attribute; it is not part of any standard.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <input type="checkbox" role="switch"> | – | yes | data-slean="switch" marks it for the styles. The WebKit-only switch attribute is not used. |
| label | <label> wrapping the input, or for and id | – | yes | Names the switch. |
Runtime profile
The runtime block reads the tier and the events from the contract and the bytes from the
native-only consumer fixture: a production Vite build with the Vite plugin whose module graph
contains no @svelte-lean/core or @svelte-lean/primitives module. The
fixture's markup is button, dialog and popover; the switch path is proven the same way by
construction: packages/primitives/src/switch has no behavior, validate or register
module, the plugin maps switch to no module, and the playground's bundle spec asserts
that the native page, which includes this markup, loads no behavior runtime. Nothing is attached at
hydration.
Accessibility contract
- An
<input type="checkbox" role="switch">with a label: a wrapping<label>, orforandid. aria-checkedis not needed: the native checked state is exposed through the role.disabled: not focusable and not submitted.- Text direction is native; the thumb moves along the inline axis with logical properties in the stylesheet.
Keyboard
| Key | When | Result |
|---|---|---|
| Space | focus on the switch | Toggles (native). Enter does not, as for any checkbox |
| Tab/Shift+Tab | anywhere | Moves focus (native) |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
role="switch" on a checkbox | Defined by ARIA 1.2 and mapped by current screen readers | Announced as a checkbox where the role is not mapped |
appearance: none, :checked | Widely available | Not applicable within the support policy |
Without JavaScript
Fully functional. The playground's native page runs the Space and label assertions with page
JavaScript disabled. What is missing without a script is the immediate effect the application
would attach to change, which is an application concern.
Server rendering
The checked attribute renders the initial state. Ids are authored when for is used.
Before hydration
The delayed-hydration test clicks the switch and asserts it is checked while every script response is held back. Hydration attaches nothing to an input the package does not bind.
Styling
switch.css sets appearance: none, draws the track on the input and the
thumb on ::before, moves the thumb with inset-inline-start, and on
coarse pointers adds an invisible hit area on ::after. The motion uses the duration
tokens, which the tokens file sets to zero under prefers-reduced-motion, so the
thumb stops animating without an extra rule. The tokens it reads and the states it targets:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-radius-full | 9999px | border-radius |
--slean-control-border | var(--slean-border-strong) | background |
--slean-duration-fast | 100ms | transition |
--slean-ease | cubic-bezier(0.2, 0, 0, 1) | transition |
--slean-surface | oklch(100% 0 0) | background |
--slean-shadow-sm | 0 1px 2px oklch(0% 0 0 / 0.08) | box-shadow |
--slean-accent | oklch(54% 0.19 258) | background |
--slean-control-bg-disabled | var(--slean-muted) | background |
--slean-control-border-disabled | var(--slean-border) | box-shadow |
--slean-border-strong | var(--slean-neutral-8) | background |
--slean-space-2 | 0.5rem | gap |
--slean-fg | var(--slean-neutral-12) | color |
--slean-text-sm | 0.875rem | font-size |
--slean-leading | 1.5 | line-height |
--slean-fg-muted | var(--slean-neutral-11) | color, background |
--slean-accent-hover | oklch(49% 0.19 258) | background |
--slean-control-height-md | 2.25rem | inset |
State selectors the stylesheet targets, all from the platform or ARIA: :checked, :disabled, :hover.
Controlled integration
The input is the state. Svelte reads and writes it with bind:checked; the
application acts on the native change event because a switch takes effect at once.
The package mirrors nothing and dispatches no slean:* event for a switch; a Svelte adapter
is not built and is not needed for this primitive.
<script lang="ts">
// A switch takes effect immediately, so the application acts on change rather than on
// submit. The checked state stays in the input; bind:checked reads it.
let notifications = $state(true);
function onchange() {
void fetch('/api/preferences', {
method: 'PATCH',
body: JSON.stringify({ notifications })
});
}
</script>
<label>
<input
type="checkbox"
role="switch"
data-slean="switch"
bind:checked={notifications}
{onchange}
/>
Email notifications
</label>Compatibility notes
Nothing beyond the checkbox platform features is required, and those are Baseline widely
available. Where a screen reader does not map role="switch" on a checkbox, the
control is announced as a checkbox and keeps working. The policy is on Browser support.
Testing
apps/playground/tests/primitives/native.spec.tsPlaywright, with page JavaScript enabled and disabledapps/playground/tests/primitives/a11y.spec.tsaxe on every playground page and on open dialog, popover and menu statespackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)fixtures/native-onlyconsumer build asserting that no behavior runtime ships (invariant A)
Source
packages/primitives/src/switch/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/switch/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/switch.cssthe optional stylesheet