Slider
Slider is the continuous-range control: a recessed SurfaceCut track, a gradient thumb that takes the theme accent, and a floating value bubble that lifts above the thumb while the user drags or focuses. Reach for it when the user cares about the proportion more than the exact digit — volume, brightness, opacity, font size, zoom, mix levels. A real <input type="range"> lives underneath, so arrow keys, Home/End, screen readers, and form submission all work out of the box.
<Slider value={volume} onChange={setVolume} className="w-72" />
<Slider
value={bright}
onChange={setBright}
color="yellow"
size="md"
className="w-72"
/>
<Slider
value={opacity}
onChange={setOpacity}
min={0}
max={1}
step={0.01}
color="purple"
className="w-72"
/>Usage
import { Slider } from '@cladd-ui/react';
<Slider value={volume} onChange={setVolume} />;Slider works in both controlled and uncontrolled modes. Pass value + onChange for controlled, or defaultValue for uncontrolled. onChange fires on every step while the user drags or arrows through values — pair it with debounce or throttle when the consumer is expensive (a network call, a heavy re-render in a sibling pane).
variant chooses between two visual presentations. The default 'thumb' is the thin track with a prominent draggable thumb described above. The newer 'track' is a thick, recessed track that is the control — the filled portion does the talking and a slim flush handle replaces the round thumb. Reach for 'track' when the slider reads as a chunky meter or fill bar (a volume block, a progress-shaped input) rather than a hairline scrubber.
Slider, NumberField, or NumberScrubber?
Reach for Slider when the value is continuous and the user benefits from seeing where they are in the range — a 30 / 100 brightness reads at a glance from the filled segment. Reach for NumberField when the value is discrete and the user mostly nudges in steps (quantity, tip percent, page count) — the + / − buttons own the writes. Reach for NumberScrubber when the value is arbitrary precision and the user wants both a draggable handle and a typeable input in the same compact slot — the inspector pattern from design tools (16px, 1.45, -0.4).
Examples
Sizes
size accepts xs, sm (default), and md. Like its siblings Checkbox and Radio, Slider rides the thumb-scale ramp instead of the full 2xs → 2xl scale — a track-and-thumb control doesn't earn its keep at button sizes. sm (20px thumb) is the inline default; reach for md (24px) when the slider is the focal control of its row or the surrounding controls are md-sized; reach for xs (16px) in tight inspector rows where every pixel of vertical space matters.
<Slider size="md" value={a} onChange={setA} className="w-72" />
<Slider size="md" value={b} onChange={setB} className="w-72" />Colors
color accepts any of the eleven cladd accent tokens — the accent drives both the filled segment of the track and the thumb's gradient fill. When unset, the slider inherits the theme accent from CladdProvider, so a brand-coloured slider doesn't usually need a color prop. Pass one when the slider signals semantics (red for "destructive volume", yellow for brightness) or when it sits alongside a non-default control that needs to read as a single unit.
<Slider
color="brand"
value={value}
onChange={setValue}
className="w-72"
/>Min, max, step
min (default 0), max (default 100), and step (default 1) define the value space. The defaults are calibrated for percentage-shaped values; override all three for normalised ranges (0 → 1 with step={0.01} for opacity), wide ranges (25 → 400 with step={25} for zoom levels), or signed ranges (-2 → 8 for letter spacing). The thumb snaps to step on drag and on keyboard arrow presses.
<Slider value={pct} onChange={setPct} step={5} className="w-72" />
<Slider
value={opacity}
onChange={setOpacity}
min={0}
max={1}
step={0.01}
color="purple"
className="w-72"
/>
<Slider
value={zoom}
onChange={setZoom}
min={25}
max={400}
step={25}
color="cyan"
className="w-72"
/>Scale
scale controls how user values map to thumb position on the track. The default 'linear' is right for percentages, opacity, brightness — anything where equal pixel movement should feel like equal numerical change. Reach for 'log' (requires min > 0) when the user cares about ratios, not absolute differences: an audio frequency picker spans 20 Hz → 20 kHz, but the interesting region for human hearing is the lower decade, so a linear track wastes most of its travel on near-inaudible high frequencies. Compare the two below — the logarithmic slider gives 1 kHz about half the track instead of 5% of it.
For curves that aren't logarithmic — quadratic, S-curve, custom price brackets — pass a { toSlider, fromSlider } pair instead, where toSlider(value) → position returns 0..1 and fromSlider(position) → value is the inverse. min, max, and value stay in user-value space, and step is still applied after the inverse mapping, so the emitted value stays grid-aligned.
<div className="flex w-72 flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex items-baseline justify-between">
<SectionTitle>Linear</SectionTitle>
<span className="text-cladd-fg-soft tabular-nums">
{linearHz.toLocaleString()} Hz
</span>
</div>
<Slider
value={linearHz}
onChange={setLinearHz}
min={20}
max={20000}
color="cyan"
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-baseline justify-between">
<SectionTitle>Logarithmic</SectionTitle>
<span className="text-cladd-fg-soft tabular-nums">
{logHz.toLocaleString()} Hz
</span>
</div>
<Slider
value={logHz}
onChange={setLogHz}
min={20}
max={20000}
scale="log"
color="cyan"
/>
</div>
</div>Value display
The slider renders a floating value bubble above the thumb while the user drags or focuses — handy for confirming the exact landing value mid-gesture. For the resting state, pair the slider with an external readout: a SectionTitle label on the left, the formatted value on the right, the track between them. Format the value however the unit calls for — raw integer for volume, xx% for normalised opacity, xxpx for size.
<div className="flex w-72 flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex items-baseline justify-between">
<SectionTitle>Volume</SectionTitle>
<span className="text-cladd-fg-soft tabular-nums">{volume}</span>
</div>
<Slider value={volume} onChange={setVolume} color="brand" />
</div>
<div className="flex flex-col gap-2">
<div className="flex items-baseline justify-between">
<SectionTitle>Opacity</SectionTitle>
<span className="text-cladd-fg-soft tabular-nums">
{Math.round(opacity * 100)}%
</span>
</div>
<Slider
value={opacity}
onChange={setOpacity}
min={0}
max={1}
step={0.01}
color="purple"
/>
</div>
</div>Debounce
debounce (in ms, default 0) delays the onChange call until the user pauses. The internal value still updates immediately, so the thumb tracks the drag with no visual lag — only the parent's onChange callback is throttled. Useful when the consumer is expensive: a colour-picker that re-renders a canvas, a search that hits an API, a Monaco editor that has to relayout. Drag the second slider below to feel the pause — the readout only catches up when you let go.
<div className="flex w-72 flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex items-baseline justify-between">
<SectionTitle>Immediate</SectionTitle>
<span className="text-cladd-fg-soft tabular-nums">{immediate}</span>
</div>
<Slider value={immediate} onChange={setImmediate} color="brand" />
</div>
<div className="flex flex-col gap-2">
<div className="flex items-baseline justify-between">
<SectionTitle>Debounced 400ms</SectionTitle>
<span className="text-cladd-fg-soft tabular-nums">{debounced}</span>
</div>
<Slider
defaultValue={debounced}
onChange={setDebounced}
debounce={400}
color="orange"
/>
</div>
</div>Throttle
throttle (in ms, default 0) is the lighter-touch sibling of debounce. The callback fires immediately on the first change, then at most once per N ms while the user keeps changing, with a final trailing call when they stop — so the consumer sees regular updates during the drag instead of one big jump at the end. Reach for it when the side effect is too expensive to run on every step but the user still benefits from live feedback (a waveform that has to re-render, a colour preview, a live-filtered list). throttle takes precedence over debounce when both are set.
<div className="flex w-72 flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex items-baseline justify-between">
<SectionTitle>Immediate</SectionTitle>
<span className="text-cladd-fg-soft tabular-nums">{immediate}</span>
</div>
<Slider value={immediate} onChange={setImmediate} color="brand" />
</div>
<div className="flex flex-col gap-2">
<div className="flex items-baseline justify-between">
<SectionTitle>Throttled 200ms</SectionTitle>
<span className="text-cladd-fg-soft tabular-nums">{throttled}</span>
</div>
<Slider
defaultValue={throttled}
onChange={setThrottled}
throttle={200}
color="green"
/>
</div>
</div>Disabled and read-only
disabled dims the whole slider to 50% and blocks all pointer and keyboard interaction — read as "not available right now" (the audio source is muted at the OS level, the layer is locked). readOnly is the subtler one: the slider keeps full opacity but stops responding to drags and keys — read as "live indicator, but you can't move it from here" (an enforced organisation policy, a value driven by a sibling control).
<div className="flex w-72 flex-col gap-2">
<SectionTitle>Disabled</SectionTitle>
<Slider defaultValue={30} disabled />
</div>
<div className="flex w-72 flex-col gap-2">
<SectionTitle>Read-only</SectionTitle>
<Slider defaultValue={70} readOnly color="red" />
</div>Inspector row
The canonical design-tool pattern: a Surface panel with a label column, a slider column, and a value column on a single grid. Each row sets its own min / max / step because the unit is different (px for size, unitless for line-height, em-fractions for letter-spacing). The shared accent colour ties the group together; the value column does the formatting. This is what cladd was designed for: dense, but not crowded.
<div className="flex w-72 flex-col gap-2">
<SectionTitle>Typography</SectionTitle>
<div className="grid grid-cols-[6rem_1fr_3rem] items-center gap-2">
<span className="text-cladd-fg-soft">Size</span>
<Slider
value={fontSize}
onChange={setFontSize}
min={8}
max={64}
color="brand"
/>
<span className="text-right text-cladd-fg-soft tabular-nums">
{fontSize}px
</span>
<span className="text-cladd-fg-soft">Line height</span>
<Slider
value={lineHeight}
onChange={setLineHeight}
min={1}
max={2}
step={0.05}
color="brand"
/>
<span className="text-right text-cladd-fg-soft tabular-nums">
{lineHeight.toFixed(2)}
</span>
<span className="text-cladd-fg-soft">Letter spacing</span>
<Slider
value={letter}
onChange={setLetter}
min={-2}
max={8}
step={0.1}
color="brand"
/>
<span className="text-right text-cladd-fg-soft tabular-nums">
{letter.toFixed(1)}
</span>
</div>
</div>Playground
size, color, and the state toggles compose freely. sm covers most cases; reach for md when the slider is the focal control of its row, and xs for very tight inspector rows.
<Slider
value={value}
onChange={setValue}
size="md"
color="brand"
disabled={false}
readOnly={false}
className="w-72"
/>Track variant
variant="track" swaps the thin-track-with-thumb for a thick recessed track that is the control itself: the active range is a raised gradient Surface inside a SurfaceCut channel, and a slim flush handle marks the value instead of a round thumb. The track grows with size, so it reads as a substantial block rather than a hairline. Unlike the thumb variant, color is left unset by default — the filled track inherits any surrounding cladd-color-* context, so a track slider drops into a coloured panel without a color prop.
<Slider
variant="track"
size="xs"
value={a}
onChange={setA}
className="w-72"
/>
<Slider
variant="track"
size="sm"
color="green"
value={b}
onChange={setB}
className="w-72"
/>
<Slider
variant="track"
size="md"
color="blue"
value={c}
onChange={setC}
className="w-72"
/>Rounded track
rounded (default false, track variant only) swaps the size-based corner radius for fully round (rounded-full) ends — a pill-shaped track and a pill-shaped fill. Reach for it when the slider sits among other rounded controls or when the softer silhouette suits the surface. It's a no-op on the thumb variant, whose track is already a thin pill.
<Slider
variant="track"
rounded
size="sm"
color="purple"
value={a}
onChange={setA}
className="w-72"
/>
<Slider
variant="track"
rounded
size="md"
color="cyan"
value={b}
onChange={setB}
className="w-72"
/>Range fill
rangeFill (default false, track variant only) renders the active range as a bold gradient-fill accent instead of the subtle raised gradient — the filled portion goes solid-colour, turning the track into a high-contrast meter. The handle flips to the on-accent colour once it crosses the midpoint so it stays legible against the fill. Pair it with rounded for a fill-bar look. rangeOutline (default true, track variant only) controls the outline ring on the active range — keep it on for a crisp edge against the channel, turn it off when the fill should read as flush.
<Slider
variant="track"
rangeFill
size="md"
color="green"
value={a}
onChange={setA}
className="w-72"
/>
<Slider
variant="track"
rangeFill
rounded
size="md"
color="blue"
value={b}
onChange={setB}
className="w-72"
/>Track playground
The track-variant flags — size, color, rounded, rangeFill, rangeOutline — compose freely. Toggle them below to dial in the exact density and contrast the surface calls for.
<Slider
variant="track"
value={value}
onChange={setValue}
size="md"
color="blue"
rounded={false}
rangeFill
rangeOutline
className="w-72"
/>API Reference
| Name: Type | Default | Description |
|---|---|---|
| className: string | — | Extra classes for the slider container. |
| color: Color | — | Accent color for the active track segment and thumb. Defaults to the theme accent for variant="thumb". For variant="track" it is left unset unless explicitly passed, so the filled track inherits any surrounding cladd-color-* context (a neutral surface otherwise). |
| debounce: number | — | Debounce onChange calls in ms. Fires once after the user stops changing for N ms. Defaults to 0 (immediate). |
| defaultValue: number | 0 | Initial value (uncontrolled). Default 0.Ignored when value is provided. |
| disabled: boolean | — | Visually dim the slider and disable interaction. |
| input: boolean | — | Reserved - currently unused in the rendered output (the underlying <input type="range"> is always present). Kept for parity with other form components. |
| max: number | 100 | Default 100. |
| min: number | 0 | Default 0. |
| onChange: (value: number, event?: ChangeEvent<HTMLInputElement>) => void | — | Fires while the user drags or types a new value (subject to debounce / throttle). |
| rangeFill: boolean | false | Render the active range as a bold filled accent (gradient-fill) instead of the subtle raised gradient surface. Default false.Only affects variant="track". |
| rangeOutline: boolean | true | Outline ring on the active range surface. Default true.Only affects variant="track". |
| readOnly: boolean | — | Block dragging without the disabled visual treatment. |
| rounded: boolean | false | Fully round the track corners (rounded-full) instead of the size-based radius. Default false.Only affects variant="track" - the thumb variant's track is already a thin pill, so this is a no-op there. |
| scale: SliderScale | 'linear' | Value-to-position mapping. Default 'linear'.Use 'log' (requires min > 0) for ranges where ratios matter more than absolute differences - audio frequency, zoom, price brackets.Pass a custom { toSlider, fromSlider } pair for any other curve (quadratic, S-curve, etc.).min, max, and value are always in user-value space. step is applied to the emitted value after the inverse mapping (see step). |
| size: SliderSize | 'sm' | Slider size token. Drives track and thumb dimensions. Default 'sm'. |
| step: number | 1 | Step size for the emitted value. Default 1.For non-linear scale, the user value is rounded to the nearest step after the inverse mapping - so a log slider with step={1} still emits integer values, but those steps are spaced logarithmically on the track. |
| throttle: number | — | Throttle onChange calls in ms. Fires immediately, then at most once per N ms while changing, with a trailing call for the final value. Defaults to 0 (immediate). Takes precedence over debounce when both are set. |
| thumbOutline: boolean | true | Outline ring on the thumb surface. Default true. |
| value: number | — | Controlled value. When omitted, the component falls back to uncontrolled mode using defaultValue. |
| variant: SliderVariant | 'thumb' | Visual presentation. Default 'thumb'. See {@link SliderVariant}. |