Installation
pnpm dlx shadcn@latest add https://dithercn.dev/r/dither-slider.jsonUsage
import { DitherSlider } from "@/components/dithercn/dither-slider";
export function Opacity() {
return <DitherSlider label="Opacity" defaultValue={[64]} unit="%" />;
}DitherSlider is built on the Radix Slider, so it takes the same value, defaultValue, onValueChange, onValueCommit, min, max, step and name props. Values are arrays, as in shadcn's slider: one value draws one thumb, two values draw a range.
The filled part of the track is a Bayer ramp worked out cell by cell. It is sparse far from the thumb, gets denser towards it, and is solid for the last two cells before a one-cell gap at the thumb, so the value reads at a glance. The cells sit on the track's own grid: dragging flips cells through their thresholds instead of sliding a texture along. With two thumbs the ramp runs in from both ends and thins out in the middle.
Hover, keyboard focus and dragging fade in a ramp twice as long with a denser floor. The thumb is a notched cap lit like Dither button: a highlight dithered down from the top, a shade dithered up from the bottom and a grip line. Hover brightens it and dragging shades it from the top, so it looks pressed.
<DitherSlider
label="Price"
defaultValue={[150, 600]}
max={1000}
step={50}
format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
showRange
/>Examples
Price filter
Two values make a range. format takes Intl.NumberFormat options, here dollars, and is used for the readout, the showRange labels under the ends and each thumb's aria-valuetext. minStepsBetweenThumbs={1} keeps the two thumbs one step apart. The product count updates from onValueChange.
Stepped volume
step={1} on a 0 to 10 scale snaps the thumb to eleven positions, and ticks marks each one under the track. Ticks inside the selected range take the full colour. This one is size="lg" in purple with a low bloom, and the mute button sets the value from outside.
Props
| Prop | Type | Default |
|---|---|---|
| label* What the slider sets. Shown above the track and used as the accessible name. | Type: string | Default – |
| value The controlled values. One value draws one thumb, two draw a range. | Type: number[] | Default – |
| defaultValue The starting values when uncontrolled. | Type: number[] | Default [min] |
| onValueChange Called on every change. onValueCommit fires once when a drag or key press ends. | Type: (value: number[]) => void | Default – |
| min The start of the scale. | Type: number | Default 0 |
| max The end of the scale. | Type: number | Default 100 |
| step The interval values snap to. | Type: number | Default 1 |
| ticks Marks under the track: true for one per step (up to 101), or the values to mark. | Type: boolean | number[] | Default false |
| showValue Show the value, or both ends of a range, beside the label. | Type: boolean | Default true |
| showRange Show min and max under the ends of the track. | Type: boolean | Default false |
| hideLabel Hide the label and readout visually. Screen readers still get them. | Type: boolean | Default false |
| unit Shown after each value, such as "%". | Type: string | Default "" |
| format How values are formatted in the readout, range and aria-valuetext. | Type: Intl.NumberFormatOptions | Default { maximumFractionDigits: 1 } |
| thumbLabels Accessible names per thumb. Default: the label, or "<label>, minimum" and "<label>, maximum". | Type: string[] | Default – |
| color A palette name such as "blue" or "green", a CSS variable name, or any CSS colour. | Type: DitherColor | Default "ink" |
| size Track heights of 6, 8 and 12 pixels, with thumbs 14, 18 and 22 pixels wide, at the default `cell` of 2. Other cell sizes round each part to whole cells, so the sizes shift a little. | Type: "sm" | "md" | "lg" | Default "md" |
| bloom A soft glow in the slider's colour behind the fill and thumbs. | Type: "off" | "low" | "high" | Default "off" |
| matrix Threshold pattern of the ramp. | Type: "bayer2" | "bayer4" | "bayer8" | "blue-noise" | Default "bayer4" |
| cell Size of one dither cell in CSS pixels. Track and thumb sizes snap to whole cells. | Type: number | Default 2 |
| disabled Ignore input and drop to 45% opacity. | Type: boolean | Default false |
All other props, such as name, onValueCommit and minStepsBetweenThumbs, pass through to Radix Slider.Root. className and style go on the outer wrapper.
Notes
- Each thumb has
role="slider"witharia-valuenow,aria-valuemin,aria-valuemaxand anaria-valuetextthat includes the unit or currency. Arrow keys step, Page Up and Page Down step by ten, and Home and End jump to the ends. - Inside a
<form>, the slider submits one hidden input per thumb undername(name[]for a range). - Focus shows a notched ring around the thumb with
:focus-visible, and thickens the ramp just as hover does, so keyboard users see the same states as pointer users. - With reduced motion the ramp and thumb states switch without fading. Nothing else moves.
- The fill is one SVG path per state, rebuilt as the value changes; the thumb is CSS masks in one shared
<style>tag. No canvas or WebGL. Before the track is measured, a CSS ramp stands in. - The slider is horizontal and left to right only.