API
Switch
A checkbox with `role="switch"`, drawn as a toggle.
Installation
pnpm dlx zazz-ui add switchnpx zazz-ui add switchyarn dlx zazz-ui add switchbunx zazz-ui add switchRun zazz-ui init first if the project has no zazz/ folder yet.
Examples
<fieldset data-ui="field-group">
<legend>Notifications</legend>
<div
style="
--display: flex-col;
--gap: 2;
"
>
<label data-ui="field" data-field-orientation="horizontal">
<input type="checkbox" role="switch" name="notifications-email" />
<span data-field-slot="label">Email notifications</span>
</label>
<label data-ui="field" data-field-orientation="horizontal">
<input type="checkbox" role="switch" name="notifications-2fa" checked />
<span data-field-slot="label">Two-factor authentication</span>
</label>
</div>
</fieldset>/**
* switch.css — Switch (input[type="checkbox"][role="switch"])
*
* @layer variables, reset
* @requires _layers.css, _variables.css, _properties.css (--_*-resolved, --_focus-shadow)
* @uses appearance: none on input[role="switch"]
* @uses label:has(> input[role="switch"])::after — thumb drawn on label (no JS)
* @uses oklch(from …) — checked/hover track tints
* @tokens --ui-switch-* (@layer variables; consumed in @layer reset)
*/
@layer variables {
:root {
--ui-switch-track-w: calc(var(--spacing) * 7);
--ui-switch-track-padding: calc(var(--spacing) * 0.5);
--ui-switch-track-h: calc(var(--ui-switch-track-w) / 2 + var(--ui-switch-track-padding));
--ui-switch-thumb-bg: var(--color-background);
--ui-switch-thumb-bg--hover: var(--color-primary-foreground);
--ui-switch-thumb-size: calc((var(--ui-switch-track-w) / 2) - var(--ui-switch-track-padding));
--ui-switch-thumb-shadow: var(--shadow-xs);
--ui-switch-thumb-translate: var(--ui-switch-track-padding), -50%;
--ui-switch-thumb-scale: 1;
--ui-switch-track-bg: var(--color-muted);
--ui-switch-track-bg--hover: oklch(from var(--color-muted) l c h / calc(alpha + 0.04));
--ui-switch-track-bg--checked: var(--color-primary);
--ui-switch-track-bg--checked-hover: var(--color-primary);
--ui-switch-rounded: var(--radius-full);
}
}
@layer reset {
/* ===========================================================================
SWITCH — native <input type="checkbox" role="switch"> internals
In the reset layer so component overrides win.
=========================================================================== */
:where(label:has(> input[role="switch"])) {
position: relative;
&::after {
content: "";
position: absolute;
inset-block-start: 50%;
inset-inline-start: 0;
inline-size: var(--ui-switch-thumb-size);
block-size: var(--ui-switch-thumb-size);
border-radius: var(--radius-full);
background-color: var(--ui-switch-thumb-bg);
box-shadow: var(--ui-switch-thumb-shadow);
pointer-events: none;
transform: translate(var(--ui-switch-thumb-translate)) scale(var(--ui-switch-thumb-scale));
transition:
box-shadow var(--default-transition-duration) var(--default-transition-timing-function),
transform var(--default-transition-duration) var(--default-transition-timing-function);
z-index: 1;
}
&:has(> input[role="switch"]:checked)::after {
background-color: var(--ui-switch-thumb-bg--hover);
--ui-switch-thumb-translate:
calc(var(--ui-switch-track-padding) + var(--ui-switch-thumb-size)), -50%;
}
@media (hover: hover) {
&:has(> input[role="switch"]:hover):after {
--ui-switch-thumb-scale: 1.1;
}
}
&:has(> input[role="switch"]:active):after {
--ui-switch-thumb-shadow: none;
--ui-switch-thumb-scale: 0.98;
}
}
:where(label:has(> input[role="switch"]:disabled)) {
opacity: 0.5;
cursor: not-allowed;
}
:where(label:has(> input[role="switch"]:disabled) > input[role="switch"]) {
opacity: 1;
}
:where(input[role="switch"]) {
appearance: none;
flex-shrink: 0;
--_w-len: var(--_w-resolved, var(--ui-switch-track-w));
--_w-num: var(--_w-resolved, var(--ui-switch-track-w));
inline-size: calc(var(--_w-len) + var(--_w-num) * var(--spacing));
--_h-len: var(--_h-resolved, var(--ui-switch-track-h));
--_h-num: var(--_h-resolved, var(--ui-switch-track-h));
block-size: calc(var(--_h-len) + var(--_h-num) * var(--spacing));
background-color: var(--_bg-resolved, var(--ui-switch-track-bg));
border-radius: var(--_rounded-resolved, var(--ui-switch-rounded));
cursor: pointer;
/* the transparent outline keeps focus visible in forced-colors */
--_ring-offset-width: 0px;
--_ring-width: 0px;
--_focus-in: max(0, -1 * sign(var(--_focus-ring-w, 1px)));
--_focus-shadow:
0 0 0 calc(var(--_ring-offset-width) * (1 - var(--_focus-in))) var(--ring-offset-shadow-color),
0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * (1 - var(--_focus-in)))
var(--_ring-fill, var(--ring-shadow-color)),
inset 0 0 0 calc(var(--_ring-offset-width) * var(--_focus-in)) var(--ring-offset-shadow-color),
inset 0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * var(--_focus-in))
var(--_ring-fill, var(--ring-shadow-color));
box-shadow: var(--_focus-shadow, 0 0 #0000), var(--_shadow-resolved, 0 0 #0000);
outline: abs(var(--_focus-ring-w, var(--outline-width))) var(--outline-style) transparent;
outline-offset: calc(
var(--outline-offset) - var(--_focus-in) *
(2 * var(--outline-offset) + abs(var(--_focus-ring-w, var(--outline-width))))
);
transition: var(--default-transition);
}
@media (hover: hover) {
:where(input[role="switch"]:hover) {
background-color: var(--_bg-resolved, var(--ui-switch-track-bg--hover));
}
}
:where(input[role="switch"]:checked) {
background-color: var(--_bg-resolved, var(--ui-switch-track-bg--checked));
}
@media (hover: hover) {
:where(input[role="switch"]:hover:checked) {
background-color: var(--_bg-resolved, var(--ui-switch-track-bg--checked-hover));
}
}
:where(input[role="switch"]:focus-visible) {
--_ring-offset-width: var(--ring-offset-width);
--_ring-width: abs(var(--_focus-ring-w, var(--ring-width)));
outline-color: transparent;
}
}Attributes
This primitive is a composition of other primitives and utilities: it has no presets, slots, or hooks of its own. Copy its example and adapt it.
Hooks
Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.
| Hook | Default | State |
|---|---|---|
--ui-switch-rounded | var(--radius-full) | — |
--ui-switch-thumb-bg | var(--color-background) | — |
--ui-switch-thumb-bg--hover | var(--color-primary-foreground) | hover |
--ui-switch-thumb-scale | 1 | — |
--ui-switch-thumb-shadow | var(--shadow-xs) | — |
--ui-switch-thumb-size | calc((var(--ui-switch-track-w) / 2) - var(--ui-switch-track-padding)) | — |
--ui-switch-thumb-translate | var(--ui-switch-track-padding), -50% | — |
--ui-switch-track-bg | var(--color-muted) | — |
--ui-switch-track-bg--checked | var(--color-primary) | checked |
--ui-switch-track-bg--checked-hover | var(--color-primary) | checked-hover |
--ui-switch-track-bg--hover | oklch(from var(--color-muted) l c h / calc(alpha + 0.04)) | hover |
--ui-switch-track-h | calc(var(--ui-switch-track-w) / 2 + var(--ui-switch-track-padding)) | — |
--ui-switch-track-padding | calc(var(--spacing) * 0.5) | — |
--ui-switch-track-w | calc(var(--spacing) * 7) | — |