A styled native HTML checkbox that looks like the Switch and works without JavaScript.
Installation
Install the dependencies
Copy the component
Copy the following files into src/lib/components/ui/native-switch.
These files use the Nova style and Lucide icons. npx @sveltery/ui@latest add writes the style and icon library from your components.json.
<script lang="ts">
import type { NativeSwitchProps } from './types.js';
import { cn } from 'cn';
let { class: classProp, size = 'default', ...props }: NativeSwitchProps = $props();
</script>
<!-- The input is the track; the thumb is its sibling and follows `:checked` through `peer-checked`. -->
<span data-slot="switch" data-size={size} class={cn('group/switch peer relative inline-grid shrink-0 items-center *:col-start-1 *:row-start-1 has-disabled:opacity-50', classProp)}>
<input type="checkbox" role="switch" data-slot="native-switch" data-size={size} class="checked:bg-primary not-checked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:not-checked:bg-input/80 shrink-0 rounded-full border border-transparent focus-visible:ring-3 aria-invalid:ring-3 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:border-transparent data-[size=default]:h-[18px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer m-0 appearance-none transition-all outline-none disabled:cursor-not-allowed" {...props} />
<span data-slot="switch-thumb" class="bg-background dark:peer-not-checked:bg-foreground dark:peer-checked:bg-primary-foreground rounded-full group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:peer-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:peer-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:peer-not-checked:translate-x-0 group-data-[size=sm]/switch:peer-not-checked:translate-x-0 ml-px pointer-events-none block ring-0 transition-transform"></span>
</span> import type { HTMLInputAttributes } from 'svelte/elements';
export type NativeSwitchProps = Omit<HTMLInputAttributes, 'type' | 'size'> & { type?: 'checkbox'; size?: 'sm' | 'default' }; export { default as NativeSwitch } from './NativeSwitch.svelte';
export type { NativeSwitchProps } from './types.js'; Update the import paths
The component folders import each other with relative paths, so keep them side by side. If you place them elsewhere, change the imports to match your project setup.
Usage
<script lang="ts">
import { NativeSwitch } from '#lib/components/ui/native-switch';
import { Label } from '#lib/components/ui/label';
</script> <div class="flex items-center gap-2">
<NativeSwitch id="airplane-mode" name="airplane" />
<Label for="airplane-mode">Airplane Mode</Label>
</div> NativeSwitch is a real <input type="checkbox" role="switch">, and every prop goes on it. Mark the initial state with defaultChecked; checked sets the current state, which is what a remote form field passes. size takes default or sm.
The input is the track, and the thumb is a sibling that follows :checked. Both sit in a <span> that takes class and carries the Switch's data-slot="switch", so a Label or Field styles itself as it does next to a Switch.
When to use it
Use NativeSwitch when the form must work before the page hydrates or with JavaScript off. A click on the switch or its label, or Space, toggles it, and a checked switch submits with its form. Screen readers announce it as a switch.
Switch runs on the Sveltery Base Switch, which also gives you Base UI state attributes such as data-checked, bind:checked and onCheckedChange, and Base's Field behavior.
Remote forms
Spread a boolean remote form field onto the switch with as('checkbox'). See the example on Native Checkbox.
<Field orientation="horizontal">
<FieldLabel for="digest">Weekly digest</FieldLabel>
<NativeSwitch id="digest" {...f.fields.digest.as('checkbox')} />
</Field>