Skip to content
Drivn
7 min read

React Slider Component Examples

Copy-paste React Slider examples: volume control, brightness label, vertical opacity dial, step snapping, sm/md/lg sizes, disabled state, and form submission.

A slider is the right control whenever the value is a continuous number on a known range — volume from zero to one hundred, brightness from off to full, opacity from transparent to opaque, a quality setting between low and high. The Slider component in Drivn is a single 120-line client file that reads pointer events off the track, snaps the value to the nearest step, and calls onChange with a plain number. No Radix dependency, no compound sub-components, no array values — one thumb, one number, one callback.

Every example on this page imports the component from @/components/ui/slider — the path the Drivn CLI installs it under — and pairs it with useState in a 'use client' page or panel. The Slider accepts value (controlled) or defaultValue (uncontrolled), min, max, step, orientation ('horizontal' | 'vertical'), size ('sm' | 'md' | 'lg'), disabled, and a name prop that wires it into form submission via a hidden range input. Every snippet is TypeScript because Drivn ships TypeScript-only.

The examples below cover a volume control with a live label, a brightness setting with a percent readout, a price filter, a vertical opacity dial, the three built-in size variants, step snapping, a disabled state, and a form-submission pattern that posts the value to a server action. The full props table sits on the Slider docs page, and the shadcn/ui comparison lives on Drivn vs shadcn Slider.

A volume control with a live label

The canonical Slider use is a single-thumb control with a number readout sitting just above or below. Hold volume in useState, pass it as value, pass setVolume as onChange, and let the component handle every drag, click, and snap on the track. The min={0} max={100} defaults are already what a percentage control wants, so this is the smallest possible setup.

The label updates inline because setVolume runs on every pointer move — the component's handlePointerDown resolver fires for both the initial click and every pointermove until pointerup. That means the readout tracks the thumb in real time without any throttling or debounce on the consumer side. Wrap the whole panel in a Card for a settings row, or drop it straight into a Popover for a header-bar volume toggle.

1"use client"
2
3import { useState } from "react"
4import { Slider } from "@/components/ui/slider"
5
6export default function VolumePanel() {
7 const [volume, setVolume] = useState(50)
8
9 return (
10 <div className="w-full space-y-2">
11 <div className="text-center text-sm font-medium">
12 Volume {volume}
13 </div>
14 <Slider value={volume} onChange={setVolume} />
15 <div className="flex justify-between text-xs text-muted-foreground">
16 <span>0</span>
17 <span>100</span>
18 </div>
19 </div>
20 )
21}

A brightness setting with a percent readout

The same pattern as the volume control, dressed for a settings row: a Label on the left, the Slider in the middle, the percentage on the right. Use htmlFor on the label and a matching id on the Slider for the accessibility wiring. The wrapper is a flex row with items-center gap-4 so the three pieces line up vertically; the Slider stretches to fill the remaining space via flex-1.

The readout uses a fixed-width tabular-nums text node so the digits do not jitter when the value crosses single → double → triple digits. For a row of settings — brightness, contrast, saturation, hue — stack three or four of these inside a single Card. Each Slider keeps its own useState, or share a single object state if your settings save together.

1"use client"
2
3import { useState } from "react"
4import { Label } from "@/components/ui/label"
5import { Slider } from "@/components/ui/slider"
6
7export default function BrightnessRow() {
8 const [brightness, setBrightness] = useState(75)
9
10 return (
11 <div className="flex items-center gap-4">
12 <Label htmlFor="brightness" className="w-24">
13 Brightness
14 </Label>
15 <Slider
16 id="brightness"
17 value={brightness}
18 onChange={setBrightness}
19 className="flex-1"
20 />
21 <span className="w-12 text-right text-sm tabular-nums text-muted-foreground">
22 {brightness}%
23 </span>
24 </div>
25 )
26}

A vertical opacity dial

For a panel control that lives inside a toolbar or a layers sidebar, the vertical orientation reads better than a horizontal bar. Pass orientation="vertical" and the component swaps to the styles.vertical ruleset: the base height is h-48, the track runs the full height, the range fills from the bottom up, and the thumb sits at left-1/2 -translate-x-1/2. The percentage maps the same way — zero at the bottom, max at the top.

The vertical Slider is also where the vTrack width in the size mapping matters. size="sm" makes the track w-1, md (default) is w-1.5, lg is w-2. Pair the vertical Slider with a small numeric input above or below it for keyboard-only editing — Drivn's Slider does not bind arrow keys to step the value, so the input gives keyboard users a path. Drop the whole assembly into a fixed-width sidebar around 80 pixels wide.

1"use client"
2
3import { useState } from "react"
4import { Slider } from "@/components/ui/slider"
5
6export default function OpacityDial() {
7 const [opacity, setOpacity] = useState(70)
8
9 return (
10 <div className="flex flex-col items-center gap-3">
11 <span className="text-xs text-muted-foreground">Opacity</span>
12 <Slider
13 value={opacity}
14 onChange={setOpacity}
15 orientation="vertical"
16 size="md"
17 />
18 <span className="text-sm font-medium tabular-nums">{opacity}%</span>
19 </div>
20 )
21}

Step snapping for quality presets

Set step={25} and the component snaps to 0 / 25 / 50 / 75 / 100. The snap helper inside the component runs Math.round((val - min) / step) * step + min on every pointer move, so the thumb visibly jumps between the discrete positions rather than gliding. This is the right pattern for quality presets ("low / medium / high / ultra"), star ratings on a 0–10 scale with a step of 1, or any input where the underlying value is enumerated and the user needs to feel the click.

The value you receive in onChange is always already snapped, so the rest of your code can treat the slider as a typed enum lookup: ["low", "medium", "high", "ultra"][quality / 25]. Combine step with min and max for non-percentage ranges — min={1} max={5} step={1} gives a five-position rating control, min={-50} max={50} step={5} gives an eleven-position centered scale.

1"use client"
2
3import { useState } from "react"
4import { Slider } from "@/components/ui/slider"
5
6const presets = ["low", "medium", "high", "ultra"] as const
7
8export default function QualityPicker() {
9 const [quality, setQuality] = useState(50)
10 const preset = presets[quality / 25] ?? "ultra"
11
12 return (
13 <div className="space-y-2">
14 <div className="text-sm font-medium capitalize">
15 Quality: {preset}
16 </div>
17 <Slider
18 value={quality}
19 onChange={setQuality}
20 step={25}
21 />
22 </div>
23 )
24}

Three sizes side by side

The Slider ships with three built-in size tokens — sm, md, lg — that map directly to the styles.sizes object inside the component file. sm is h-1 track with a 12-pixel thumb, md is h-1.5 with a 16-pixel thumb (the default), lg is h-2 with a 20-pixel thumb. The vertical orientation uses the matching vTrack widths so the proportions stay consistent on both axes.

Use sm inside dense table rows, settings dialogs, or anywhere a single slider sits next to dense form controls. Use md as the default for stand-alone controls in a page or panel. Use lg when the slider is the primary affordance — a featured volume control on a media player, a centerpiece quality dial in an editor. Mixing sizes inside the same container is fine; the value type, the API, and the styling object are identical across all three.

1<div className="space-y-4">
2 <Slider defaultValue={40} size="sm" />
3 <Slider defaultValue={50} size="md" />
4 <Slider defaultValue={60} size="lg" />
5</div>

A disabled state for read-only previews

Pass disabled and the component applies opacity-50 cursor-default pointer-events-none to the wrapper, which freezes the thumb, dims the track, and stops every pointer event from reaching the resolver. The hidden range input keeps its current value, so a disabled Slider inside a form still submits — useful for "preview the current setting but do not let the user change it" rows in a billing page or an admin view.

The disabled visual matches the rest of the Drivn UI primitives — the same opacity floor as a disabled Button, the same cursor reset, the same pointer-events lock. Toggle the prop on a permission boolean and the row reads as informational when the user lacks edit access, and as interactive when they do.

1"use client"
2
3import { Slider } from "@/components/ui/slider"
4
5export default function PlanLimit({ canEdit }: { canEdit: boolean }) {
6 return (
7 <Slider
8 defaultValue={80}
9 disabled={!canEdit}
10 />
11 )
12}

Form submission with a hidden range input

The component renders a hidden <input type="range" name={name} value={current} min max step> next to the visual thumb, set to readOnly and tabIndex={-1} so it does not steal focus. Pass a name prop and the slider participates in standard HTML form submission — the input ships the current numeric value when the form posts to a server action or a fetch handler. For a Next.js App Router server action, this means no useState, no client onSubmit, no formData wiring beyond reading the named field.

With no name prop, the input still renders but with name="", so the field is silently dropped from the FormData. Pair the slider with a Label for the visible setting name and a submit Button for the post action. For controlled forms with react-hook-form, wire value and onChange to the Controller API and let it own the form state — the hidden input becomes optional in that flow.

1// app/settings/page.tsx — Next.js server action
2import { Slider } from "@/components/ui/slider"
3import { Button } from "@/components/ui/button"
4import { Label } from "@/components/ui/label"
5
6async function saveVolume(formData: FormData) {
7 "use server"
8 const volume = Number(formData.get("volume") ?? 0)
9 await db.user.update({ volume })
10}
11
12export default function SettingsForm() {
13 return (
14 <form action={saveVolume} className="space-y-4">
15 <Label htmlFor="volume">Default volume</Label>
16 <Slider id="volume" name="volume" defaultValue={50} />
17 <Button type="submit">Save</Button>
18 </form>
19 )
20}
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

Hold the value in useState and pass it as the value prop together with onChange. The component reads value !== undefined as the controlled signal, skips its internal state, and calls onChange on every drag step. For an uncontrolled slider, omit value and pass defaultValue instead — the component keeps the value in a useState slot internally and onChange becomes purely a notification callback.

Yes. Pass orientation="vertical" and the component swaps to the styles.vertical ruleset: base height of h-48, track running the full height, range filling from the bottom up. The size prop still applies — the matching vTrack key in styles.sizes (w-1, w-1.5, w-2 for sm/md/lg) sets the track width on the vertical axis. The current value still maps 0 → bottom, max → top.

No. Drivn's Slider takes a single number value and renders one thumb. If you need a low/high range slider — two thumbs, an array value — the shadcn/ui Slider built on @radix-ui/react-slider supports it out of the box; see the Drivn vs shadcn Slider comparison page for the trade-offs. The Drivn alternative is to compose two Slider instances with state that clamps each against the other.

Pass the step prop. The component's snap helper runs Math.round((val - min) / step) * step + min on every pointer move, so the thumb visibly jumps between positions and the onChange value is always already snapped. Use step={25} for four-position presets, step={1} on a 0–10 range for an integer rating control, step={0.1} on a 0–1 range for an opacity dial that rounds to one decimal.

Yes. Pass a name prop and the component renders a hidden <input type="range" name={name} value={current}> next to the visual thumb. The input is readOnly and tabIndex={-1} so it does not capture focus, but standard form submission picks the named field up. Works directly with Next.js server actions, plain action="/api/x" posts, and react-hook-form via Controller when you wire value and onChange.

Yes. The file starts with 'use client' because it uses useState for the uncontrolled value, useRef for the track measurement, and document-level pointermove/pointerup listeners for the drag gesture. You can still render it inside server-component pages — the parent stays server-side and the Slider hydrates on its own when the bundle reaches the browser.