Slider

Pick a value, or a span, by dragging a thumb along a track.

Four sliders on a dark screen. The first is labelled Volume with 40% on the right, its light fill ending in a pill-shaped thumb with a dark knob inside it; below are a green slider at about two thirds and an orange one at about half; the last is dimmed and labelled Locked, 30.
The pill thumb at three sizes and colours, running in the example app.

A value picked by dragging a thumb along a track, or a span picked by two.

The thumb is a pill exactly as tall as the track, so nothing has to escape the track to be drawn whole, with a knob inside it that shrinks while you drag.

Position and fill are animated on the UI thread, so dragging never round-trips through React; the value bridges back on change and again on release.

For an exact number, use NumberInput — past a handful of steps, typing beats dragging.

Installation

Slider ships with the library — no separate install.

import { Slider } from 'panelui-native';

Or copy the source into your project, to own and edit it:

npx panelui-cli@latest add slider

Usage

<Slider defaultValue={40} />
<Slider label="Volume" showValue defaultValue={40} />
<Slider value={volume} onValueChange={setVolume} />
<Slider defaultRange={[20, 80]} onRangeCommit={save} />

Examples

Controlled

Hold the value in state and read it back for a live label.

const [volume, setVolume] = useState(40);

<Slider value={volume} onValueChange={setVolume} />

With a caption row

label and showValue draw a row above the track. formatValue owns the units, so the caption reads the way the value is spoken rather than as a bare number. The label doubles as the accessibility label.

<Slider label="Brightness" showValue defaultValue={62} />

<Slider
  label="Budget"
  showValue
  formatValue={(v) => `$${Math.round(v)}`}
  min={0}
  max={1000}
  step={20}
  defaultValue={340}
  color="success"
/>

A range

defaultRange — or range for a controlled one — gives the slider two thumbs and fills between them instead of from the start. Whichever thumb your finger lands nearest is the one that moves, and the two bound each other rather than the track, so they can meet but never cross.

const [price, setPrice] = useState([220, 680]);

<Slider
  label="Price"
  showValue
  formatValue={(v) => `$${Math.round(v)}`}
  range={price}
  onRangeChange={setPrice}
  min={0}
  max={1000}
  step={20}
  color="success"
/>

Keeping the thumbs apart

minStepsBetweenThumbs is the gap the span can never close. A filter wants at least one: an empty range matches nothing, and a control that can be dragged into matching nothing reads as a bug rather than a choice.

<Slider
  label="Nights"
  showValue
  defaultRange={[2, 6]}
  min={1}
  max={14}
  step={1}
  minStepsBetweenThumbs={1}
/>

Restyling the thumb

The thumb is two elements — the coloured pill and the knob inside it — and each takes its own classes, so a custom slider does not have to give up the press animation to get a different look.

<Slider
  defaultValue={65}
  trackClassName="bg-success/15"
  fillClassName="bg-success"
  thumbClassName="bg-success"
  knobClassName="bg-white"
/>

Range and step

min, max and step bound the value and snap it to fixed increments.

<Slider min={0} max={10} step={1} defaultValue={5} />

Commit on release

onValueChange tracks the drag; onValueCommit fires once at the end — the place for a network write.

<Slider
  defaultValue={50}
  onValueChange={setPreview}
  onValueCommit={save}
/>

Haptic detents

haptics ticks under the finger each time the drag crosses onto a new step, and once more on release — the value feels like it clicks into place. It needs the optional expo-haptics, and is silent without it.

<Slider label="Zoom" showValue min={0} max={10} step={1} defaultValue={4} haptics />

Disabled

<Slider defaultValue={30} disabled />

The platform's own slider

Passing native hands the track and thumb to SwiftUI or Jetpack Compose. The caption row stays ours, so a native control still gets the label and value above it. Needs the optional @expo/ui package; without it the prop is a silent no-op.

<Slider
  native
  label="Brightness"
  showValue
  formatValue={(v) => `${Math.round(v)}%`}
  value={level}
  onValueChange={setLevel}
/>

Variants

color

  • primary (default)
  • success
  • warning
  • destructive
  • info
<Slider color="primary" defaultValue={40} />
<Slider color="success" defaultValue={40} />
<Slider color="warning" defaultValue={40} />
<Slider color="destructive" defaultValue={40} />
<Slider color="info" defaultValue={40} />

size

  • sm
  • md (default)
  • lg
<Slider size="sm" defaultValue={40} />
<Slider size="md" defaultValue={40} />
<Slider size="lg" defaultValue={40} />

API Reference

Slider

PropTypeDefaultDescription
classNamestring
valuenumberControlled value. Leave unset and pass defaultValue to run uncontrolled.
defaultValuenumber0Starting value when uncontrolled.
range[number, number]Controlled span, as [low, high]. Passing this — or defaultRange — gives the slider two thumbs and fills between them instead of from the start.
defaultRange[number, number]Starting span when uncontrolled, as [low, high].
minnumber0Lower bound.
maxnumber100Upper bound.
stepnumber1Snap granularity. The value is always a multiple of step from min.
minStepsBetweenThumbsnumber0How many steps the two thumbs must stay apart on a range slider. 0 lets them meet; 1 keeps a step between them, so the span is never empty.
onValueChange(value: number) => voidFires on every change while dragging — cheap updates only.
onValueCommit(value: number) => voidFires once when the gesture ends — the place for expensive side effects.
onRangeChange(range: [number, number]) => voidThe range equivalent of onValueChange. Only fires on a range slider.
onRangeCommit(range: [number, number]) => voidThe range equivalent of onValueCommit. Only fires on a range slider.
disabledbooleanfalse
nativebooleanRender the platform's own slider instead of this one. Requires the optional @expo/ui package; without it this prop does nothing. Theme tokens do not apply — the platform draws the control, so color, size and the slot classNames are ignored. label and showValue still render the caption row above it, since that is ours. Ignored on a range slider. Neither platform ships a two-thumb slider, so a range draws ours rather than quietly losing a thumb.
labelstringCaption above the track. Also becomes the accessibility label.
showValuebooleanfalseShow the current value on the caption row, opposite the label.
formatValue(value: number) => stringFormat the shown value. Defaults to the rounded number.
hapticsbooleanfalseA tick under the finger each time a drag crosses onto a new step, and once more when the drag ends. Off by default — needs the optional expo-haptics, and is silent without it.
headerClassNamestringExtra classes for the caption row.
trackClassNamestringExtra classes for the unfilled track.
fillClassNamestringExtra classes for the filled portion.
thumbClassNamestringExtra classes for the draggable thumb.
knobClassNamestringExtra classes for the knob inside the thumb.

Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.

Notes

Runs controlled (value + onValueChange) or uncontrolled (defaultValue). onValueChange fires on every frame of a drag; onValueCommit fires once on release — reach for it when the change is expensive. The value is always snapped to a multiple of step from min.

A range is a second pair of props, not a tuple in the first. Passing range or defaultRange gives the slider two thumbs and reports through onRangeChange / onRangeCommit. Keeping them apart is what lets a one-thumb slider's handler stay (value: number) => void — a single set of props covering both would hand every existing caller a union to narrow before they could read a number out of it.

The knob is painted with the page background rather than a per-colour on-token, because the status foregrounds are the darker text hues meant for soft fills — a green-700 knob on a green-500 pill would barely show. Override knobClassName if a theme needs something else.

See Native rendering for what native keeps and what it gives up — notably onValueCommit, which the platform control does not report. native is ignored on a range slider: neither platform ships a two-thumb control, so a range draws ours rather than quietly losing a thumb.

Public exports

Values: Slider

Types: SliderProps

On this page