Slider

Range slider from scratch. Single-thumb by default, two-thumb range with a second value, full keyboard + pointer support, three sizes, four colour variants, optional tick marks, and a value tooltip that can stay pinned or appear only while dragging. Vertical orientation supported.

Volume40
Price range20 – €80

Usage

import { Slider } from '@bwo-ui/react';

// Single thumb (controlled)
const [vol, setVol] = useState([40]);
<Slider value={vol} onValueChange={setVol} min={0} max={100} step={1} />

// Range (two thumbs)
const [range, setRange] = useState([20, 80]);
<Slider value={range} onValueChange={setRange} />

// Uncontrolled
<Slider defaultValue={[40]} onValueCommit={(v) => save(v)} />

The slider operates on number[] — pass a single-element array for one thumb, a two-element array for a range. onValueChange fires continuously while dragging; onValueCommit fires once on release (use it for save / analytics so you're not spamming requests).

Sizes

sm (3 px track / 14 px thumb) for dense settings rows, md (5 / 20 px, default) for general use, lg (7 / 26 px) for hero positions where the slider is the primary interaction.

Small (3 / 14 px)
Medium (5 / 20 px) — default
Large (7 / 26 px)
<Slider size="sm" value={[30]} onValueChange={…} />
<Slider          value={[55]} onValueChange={…} />  {/* md */}
<Slider size="lg" value={[80]} onValueChange={…} />

Variants

Four colour variants — primary (black), green, yellow, red. The variant colour drives the range fill and the thumb's focus ring, so the slider stays legible in either mode.

primary40
green65
yellow78
red92
<Slider variant="primary" value={[40]} onValueChange={…} />
<Slider variant="green"   value={[65]} onValueChange={…} />
<Slider variant="yellow"  value={[78]} onValueChange={…} />
<Slider variant="red"     value={[92]} onValueChange={…} />

Marks

Pass an array of numbers for unlabelled ticks, or an array of { value, label } objects to render labels under each tick. Marks within the active range highlight in the variant colour.

Temperature22°C
Discrete steps (1-5)Step 3
{/* Unlabelled ticks */}
<Slider min={1} max={5} step={1} marks={[1, 2, 3, 4, 5]} value={[3]} onValueChange={…} />

{/* Labelled ticks */}
<Slider
  min={14}
  max={30}
  value={[22]}
  onValueChange={…}
  marks={[
    { value: 16, label: 'Cool' },
    { value: 20, label: '20°' },
    { value: 24, label: '24°' },
    { value: 28, label: 'Warm' },
  ]}
/>

Tooltip

Show a small bubble above the thumb with the current value. Three modes: never (default — no tooltip), drag (appears while dragging or focused — keyboard-friendly), always (pinned — for dashboards where the value should always be visible).

Use the formatValue prop to control the display string. The same formatter feeds aria-valuetext on each thumb so screen readers announce the formatted value, not the raw number.

tooltip="drag" — appears while dragging or focused
tooltip="always" — pinned, useful for dashboard surfaces
<Slider tooltip="drag" formatValue={(v) => `${v}%`} value={vol} onValueChange={…} />

<Slider
  tooltip="always"
  variant="green"
  formatValue={(v) => `€${v.toFixed(0)}`}
  value={price}
  onValueChange={…}
/>

Step + largeStep

step sets the increment. largeStep sets the larger jump used by Shift+Arrow / PageUp / PageDown. Home / End jump to the min / max bounds.

Step 5, Shift+Arrow = step 2050

Tab to focus the thumb. Arrow keys step by step. Shift+Arrow or PageUp / PageDown step by largeStep. Home / End jump to the bounds.

<Slider
  value={[50]}
  onValueChange={…}
  min={0}
  max={100}
  step={5}
  largeStep={20}
  marks={[0, 25, 50, 75, 100]}
  tooltip="drag"
/>

Vertical orientation

Pass orientation="vertical" for a column-axis slider. The track measures 160 px tall by default — wrap in a parent with the height you need if that doesn't suit. The keyboard mapping mirrors horizontal: Up = increase, Down = decrease.

40Low
70Mid
55High
<Slider
  orientation="vertical"
  variant="green"
  tooltip="drag"
  value={mid}
  onValueChange={setMid}
/>

Disabled

disabled removes pointer events and reduces opacity. The thumb is tabIndex=-1 so it's skipped by keyboard navigation.

<Slider value={[40]} disabled />
<Slider value={[20, 70]} variant="green" disabled />

Recipe — volume with mute

Classic mute-button pattern. The button toggles a muted flag; the slider renders [0] while muted and is disabled. Dragging the slider while muted un-mutes (so the user can recover with a single gesture).

72%
function VolumeWithMute() {
  const [volume, setVolume] = useState([72]);
  const [muted, setMuted] = useState(false);
  return (
    <>
      <button onClick={() => setMuted((m) => !m)}>
        {muted ? 'Unmute' : 'Mute'}
      </button>
      <Slider
        value={muted ? [0] : volume}
        onValueChange={(v) => {
          setVolume(v);
          if (muted && v[0]! > 0) setMuted(false);
        }}
        disabled={muted}
      />
    </>
  );
}

Form integration

Pass name to render a hidden <input> with each thumb's value — Slider submits cleanly inside a native form. For a range slider, the inputs are emitted as name="range[0]" / name="range[1]" so PHP / Rails-style array decoders pick them up.

<form action="/save" method="post">
  <Slider name="volume" defaultValue={[40]} />
  {/* Single thumb → <input type="hidden" name="volume" value="40" /> */}

  <Slider name="range" defaultValue={[20, 80]} />
  {/* Range → name="range[0]" and name="range[1]" */}

  <button type="submit">Save</button>
</form>

Accessibility

Props

PropTypeDefaultDescription
valuenumber[]Controlled value. Pass a 1-element array for a single thumb, 2-element for a range.
defaultValuenumber[][50]Uncontrolled initial value.
onValueChange(value: number[]) => voidFires on every change while dragging or stepping. Use this for live UI updates.
onValueCommit(value: number[]) => voidFires once when the user releases the thumb or hits a key bound (Enter, Home, End). Use for save / analytics.
minnumber0Minimum value.
maxnumber100Maximum value.
stepnumber1Step granularity.
largeStepnumberLarger step for Shift+Arrow / Page keys. Defaults to `step × 10`.
size'sm' | 'md' | 'lg''md'3 / 14, 5 / 20, 7 / 26 px (track / thumb).
variant'primary' | 'green' | 'yellow' | 'red''primary'Range fill + thumb ring colour.
marksSliderMark[]Tick marks. Pass numbers (`[1, 2, 3]`) for unlabelled ticks, or `{ value, label }` objects to render labels under each tick.
tooltip'never' | 'drag' | 'always''never'`drag` shows the tooltip while dragging or focused; `always` keeps it pinned.
formatValue(value: number) => ReactNodeFormatter for the tooltip and `aria-valuetext`. Default: `String(value)`.
orientation'horizontal' | 'vertical''horizontal'Vertical sliders default to 160 px tall — set a parent height to override.
invertedbooleanReverse the value direction (right-to-left horizontal, top-to-bottom vertical).
disabledbooleanRemoves pointer events, reduces opacity, sets `tabIndex={-1}` on thumbs.
namestringWhen set, renders hidden inputs alongside each thumb so the slider submits with native forms.
aria-labelstringPassed to each thumb. Without it, thumbs get "Value 1" / "Value 2" as a fallback.
…restHTMLAttributes<HTMLSpanElement>All native span attributes are forwarded — `style`, `className`, `aria-*`.