Labeled Slider
A horizontal range slider with a label on the left and a live value readout on the right. The track fill is a CSS-only linear-gradient driven by a single --cb-ls-pct variable — no JavaScript-driven layout, just a value the React component updates on change.
60%
Customize
Value
60
Appearance
0
1
Installation
npx shadcn@latest add https://craftbits.dev/r/labeled-slider.jsonUsage
Uncontrolled — give it min, max, an optional defaultValue, and let it manage its own state:
import { LabeledSlider } from "@craft-bits/core";
<LabeledSlider label="Opacity" min={0} max={100} step={1} unit="%" defaultValue={60} />Controlled — pair value with onValueChange:
const [opacity, setOpacity] = useState(60);
<LabeledSlider
label="Opacity"
min={0}
max={100}
step={1}
unit="%"
value={opacity}
onValueChange={setOpacity}
/>Use format to fully control the readout — handy for ratios, decimals, or units that aren't a simple suffix:
<LabeledSlider
label="Volume"
min={0}
max={1}
step={0.01}
defaultValue={0.5}
format={(v) => v.toFixed(2)}
/>Understanding the component
- Native range under the hood. A real
<input type="range">ships keyboard support (Arrow keys, Home/End, PageUp/PageDown) and screen-reader semantics for free. We add the label and readout as siblings — norole="slider"re-implementation. - Token-driven track fill. The track is a single linear-gradient with two stops — the breakpoint sits at
var(--cb-ls-pct), which the React component sets inline from(value - min) / (max - min). No DOM measurement, no extra elements, no JS animation loop. - Three sizes via CSS variables. The
sizeprop swaps the track height and thumb diameter through--cb-ls-track-hand--cb-ls-thumb. The pseudo-element rules read those vars, so the same recipe scales without re-stamping vendor-prefixed CSS. - Controlled + uncontrolled.
value+onValueChangefor controlled,defaultValuefor uncontrolled (defaults tomin). The shape mirrors Radix's other input primitives. - Single style injection. Vendor-prefixed thumb / track rules can't live in Tailwind, so they're written to a single
<style id="cb-labeled-slider-styles">injected on first mount. A sentinel id makes it idempotent across remounts. - Tabular readout. The value uses
font-variant-numeric: tabular-numsso digits don't jitter as the value changes.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
min | number | required | Minimum value. |
max | number | required | Maximum value. |
step | number | 1 | Step granularity. |
value | number | — | Controlled value. Pair with onValueChange. |
defaultValue | number | min | Uncontrolled initial value. |
onValueChange | (value: number) => void | — | Called with the new numeric value on every drag / key step. |
label | ReactNode | — | Label shown top-left. ReactNode so callers can pass icons. |
unit | string | — | Suffix shown next to the value (e.g. "%", "px"). Ignored when format is provided. |
format | (value: number) => string | — | Full formatter for the value readout — wins over unit. |
size | 'sm' | 'md' | 'lg' | 'md' | Track height + thumb diameter. |
disabled | boolean | false | Disable the slider. |
className | string | — | Merged onto the outer wrapper <div>. |
Accessibility
- Renders a native
<input type="range">—role="slider", arrow-key stepping, Home / End jump to bounds, and PageUp / PageDown step in larger increments. All of this is browser-native; no JS rebuilding. - The visible label is a real
<label htmlFor={id}>linked to the input via a generateduseId(), so screen readers announce the label as the input's accessible name. aria-valuemin,aria-valuemax,aria-valuenow, andaria-valuetextare mirrored from the React state so assistive tech announces the formatted value (including unit) — not just the raw number.- Focus is visible via a token-driven ring around the thumb (
--cb-accent-mutedhalo) so keyboard users can always see the active control. - Color contrast: the label uses
--cb-fg-muted, the value uses--cb-fg, and the filled track sits on--cb-accent— all pass WCAG AA in the default theme.
Credits
- Extracted from:
craftingattention(app/src/components/ui/LabeledSlider.tsx). The original used project-specific--color-ink-*variables and an external.slider-accentCSS class; craft-bits rewires it to--cb-*tokens, ships the thumb / track styles in-component (no global stylesheet required), and addssizevariants, controlled-mode support, and a tabular-nums readout.