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.json

Usage

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

  1. 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 — no role="slider" re-implementation.
  2. 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.
  3. Three sizes via CSS variables. The size prop swaps the track height and thumb diameter through --cb-ls-track-h and --cb-ls-thumb. The pseudo-element rules read those vars, so the same recipe scales without re-stamping vendor-prefixed CSS.
  4. Controlled + uncontrolled. value + onValueChange for controlled, defaultValue for uncontrolled (defaults to min). The shape mirrors Radix's other input primitives.
  5. 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.
  6. Tabular readout. The value uses font-variant-numeric: tabular-nums so digits don't jitter as the value changes.

Props

PropTypeDefaultDescription
minnumberrequiredMinimum value.
maxnumberrequiredMaximum value.
stepnumber1Step granularity.
valuenumberControlled value. Pair with onValueChange.
defaultValuenumberminUncontrolled initial value.
onValueChange(value: number) => voidCalled with the new numeric value on every drag / key step.
labelReactNodeLabel shown top-left. ReactNode so callers can pass icons.
unitstringSuffix shown next to the value (e.g. "%", "px"). Ignored when format is provided.
format(value: number) => stringFull formatter for the value readout — wins over unit.
size'sm' | 'md' | 'lg''md'Track height + thumb diameter.
disabledbooleanfalseDisable the slider.
classNamestringMerged 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 generated useId(), so screen readers announce the label as the input's accessible name.
  • aria-valuemin, aria-valuemax, aria-valuenow, and aria-valuetext are 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-muted halo) 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-accent CSS class; craft-bits rewires it to --cb-* tokens, ships the thumb / track styles in-component (no global stylesheet required), and adds size variants, controlled-mode support, and a tabular-nums readout.