Installation
pnpm dlx shadcn@latest add https://dithercn.dev/r/dither-switch.jsonUsage
import { DitherSwitch } from "@/components/dithercn/dither-switch";
export function AutoDeploy() {
return (
<div className="flex items-center gap-3">
<DitherSwitch id="auto-deploy" color="blue" defaultChecked />
<label htmlFor="auto-deploy" className="text-sm">
Auto-deploy
</label>
</div>
);
}DitherSwitch is the Radix Switch: a <button role="switch"> that you control with checked and onCheckedChange, or leave uncontrolled with defaultChecked. Space and Enter toggle it. Give it a name with a <label htmlFor> or aria-label.
Off, the track is a faint grey tint with a sparse grey texture inside a hairline. Turning it on fills the track from the left in five layers: four copies of the colour masked to denser and denser Bayer levels, then the full lit surface, the colour with a highlight dithered down from the top and a shade dithered up from the bottom, as on Dither button. Each layer starts a beat after the one below it, so sparse cells run just behind the thumb and the dense ones fill in after. Turning it off runs the wave backwards: the solid surface pulls back with the thumb and the sparse levels drain after it.
The thumb is a light knob lit the same way, over a dithered drop shadow. Hover brightens the knob and the track, and pressing shades the knob from the top.
Examples
Settings with a form
A list of labelled switches with descriptions inside a <form>. Each switch has a name, so it submits as a checkbox would: on when checked, absent when not. The first row is disabled; like a disabled checkbox, it is left out of what the form sends. Each description is tied to its switch with aria-describedby. Toggle a few and press Save.
Fills, colours and sizes
Every fill in four colours, then the three sizes. fill sets the texture of the shade when on and of the track when off: gradient is a Bayer ramp up from the bottom, dotted a staggered dot lattice, hatched rising diagonals, and solid a thin dithered rim. Toggle any of them to watch the wave.
Props
| Prop | Type | Default |
|---|---|---|
| checked Whether it is on, when you control it. Pair with onCheckedChange. | Type: boolean | Default – |
| defaultChecked Whether it starts on, when it is uncontrolled. | Type: boolean | Default false |
| onCheckedChange Called with the new state on every toggle. | Type: (checked: boolean) => void | Default – |
| color Colour of the ink when on: a palette name such as "blue" or "green", a CSS variable name, or any CSS colour. | Type: DitherColor | Default "ink" |
| fill Texture of the shade when on and of the track when off: a Bayer ramp, a dot lattice, diagonals, or a thin rim. | Type: "gradient" | "dotted" | "hatched" | "solid" | Default "gradient" |
| size Tracks of 34 by 20, 42 by 24 and 54 by 30 pixels, rounded to whole cells. | Type: "sm" | "md" | "lg" | Default "md" |
| bloom A soft glow in the ink's colour that follows the cells when on. | Type: "off" | "low" | "high" | Default "off" |
| cell Size of one dither cell in CSS pixels. | Type: number | Default 2 |
| disabled Ignores clicks and keys and drops to 45% opacity. | Type: boolean | Default false |
| name Submits the switch with its form. Use value to send something other than on, and required to make it mandatory. | Type: string | Default – |
All other props pass through to the Radix Switch.Root button.
Notes
- Focus shows a 2 pixel outline with
:focus-visibleand the hover highlight, so keyboard users see the same step as pointer users. - The clip of every layer and the thumb move on a staircase easing, one step per cell that follows the ease-out curve, so the cells never sit between pixels mid-move. Browsers without CSS
linear()easing use even steps instead. - The hit area reaches a few pixels past the track, so the small size is still easy to tap.
- With reduced motion, the switch changes state at once: no wave, no slide, no fades.
- The surface is plain CSS masks and clips in one shared
<style>tag. There is no canvas and no WebGL, so a settings page with dozens of switches stays cheap.