Next.js Slider with the App Router
Add a range slider to a Next.js App Router app. Drivn ships a copy-paste, pointer-driven Slider with a hidden range input for forms and zero runtime deps.
A slider is the drag-to-set control behind a volume knob, a price filter, or a brightness setting — the reader grabs a thumb and slides it along a track to pick a number in a range. The detail worth knowing up front in a Next.js App Router project is that Drivn's Slider is interactive: it tracks a value in React.useState, reads pointer coordinates off a ref, and listens for pointermove on the document while you drag. That makes it a Client Component — slider.tsx opens with a 'use client' directive, and it has to render inside a client boundary, not straight in a Server Component the way a purely presentational primitive would.
After install the component sits in src/components/ui/slider.tsx as a single forwardRef function. You hand it a min, a max, and a step, and it maps the thumb position to a snapped value between them; value plus onChange runs it controlled, defaultValue runs it uncontrolled. Alongside the visible track it renders a hidden <input type="range" name={...}>, so the current value posts with a form the same way a native range input would — which matters when the surrounding page uses a Next.js Server Action.
This guide installs Drivn in a Next.js 16 project, renders the Slider inside a Client Component, drives it with state, wires it into a form that a Server Action reads, and restyles the track. For the full reference see the Slider docs; for the shadcn/ui comparison see Drivn vs shadcn/ui Slider.
Install in a Next.js 16 project
Drivn installs through a small CLI that writes component source directly into your repository — there is no runtime package to version-lock. From the root of your Next.js 16 project run npx drivn add slider. The Slider imports nothing past React and the local cn utility, so the CLI writes a single slider.tsx file and leaves the rest of your tree untouched. The CLI reference documents every flag, including targeting a custom directory or installing several components at once. After install you own the file, and future Drivn releases will not overwrite it — commit it for a clean baseline before wiring it into a page. Because the component is a single self-contained file, there is no peer dependency to add and nothing to configure past having Tailwind v4 already processing your styles.
1 # from the root of your Next.js 16 project 2 npx drivn add slider
Render the slider in a Client Component
Because the Slider tracks its value in state and handles pointer events, slider.tsx opens with a 'use client' directive. In the App Router that means it must live inside a Client Component — either a file of your own that starts with 'use client', or a Server Component page that imports one. You cannot call useState for the value in a Server Component, so the usual shape is a small client wrapper that owns the state and renders the Slider. The example below is that wrapper: a 'use client' file that holds a volume value and passes it down. Import it into any Server Component page and the interactive island stays scoped to just this control while the rest of the route renders on the server. The verbatim usage mirrors the Slider docs example — value and onChange are the only props it needs to run controlled.
1 'use client' 2 3 import { useState } from 'react' 4 import { Slider } from '@/components/ui/slider' 5 6 export function VolumeSlider() { 7 const [volume, setVolume] = useState(50) 8 9 return ( 10 <Slider value={volume} onChange={setVolume} /> 11 ) 12 }
Control the value with state
The Slider works controlled or uncontrolled, and it decides which by whether you pass a value prop. Pass value together with onChange and the component is controlled — it renders exactly the number you hand it and calls onChange with the next snapped value on every drag, leaving you to store it in state. Omit value, pass defaultValue instead, and it runs uncontrolled, keeping its own internal state while still firing onChange so you can react to changes without owning them. Controlled is the right default when another part of the UI reads the value — a number label above the track, a live preview, a second synced control (the Slider docs list every prop). The snapped value respects min, max, and step: with step={10} on a 0–100 range the thumb lands on the nearest ten, so onChange only ever reports 0, 10, 20, and so on. The example reads the live value into a label above the track.
1 'use client' 2 3 import { useState } from 'react' 4 import { Slider } from '@/components/ui/slider' 5 6 export function PriceFilter() { 7 const [price, setPrice] = useState(40) 8 9 return ( 10 <div className="w-full space-y-2"> 11 <div className="text-sm font-medium">${price}</div> 12 <Slider value={price} onChange={setPrice} step={10} /> 13 </div> 14 ) 15 }
Post the value with a Server Action
Alongside the visible track, the Slider renders a hidden <input type="range" name={name}> that mirrors the current value. Give the Slider a name prop and that hidden input carries the value into any surrounding <form> — including a form wired to a Next.js Server Action. When the form submits, the value arrives in FormData under the name you set, so you read it server-side with formData.get('volume') the same way you would a native range input. The example below is the client field that owns the Slider state and sets name="volume"; drop it inside a <form action={saveVolume}> in a Server Component page, add a submit button, and the Server Action receives the number with no client-side value plumbing to the server. Because the hidden input is a real form control, the value posts whether the form submits through an action or the platform's native submission.
1 'use client' 2 3 import { useState } from 'react' 4 import { Slider } from '@/components/ui/slider' 5 6 export function VolumeField() { 7 const [volume, setVolume] = useState(50) 8 9 return ( 10 <Slider 11 name="volume" 12 value={volume} 13 onChange={setVolume} 14 /> 15 ) 16 }
Size, orientation, and restyle the track
The Slider ships three sizes and two orientations, all as props. size is 'sm' | 'md' | 'lg' and scales the track height and thumb diameter together — sm is a 1-pixel track with a 3-unit thumb, lg a 2-pixel track with a 5-unit thumb. orientation="vertical" rotates the whole control: the track runs top to bottom inside a default h-48 column and the thumb slides up for higher values. Everything visible is a Tailwind class in the styles object inside the file you own, so recoloring is a one-line edit. The track is bg-border, the filled range is bg-foreground, and the thumb is bg-foreground — swap the range and thumb to bg-primary for a branded control and every Slider in the project follows. Because those classes read your theme tokens, the control matches dark and light mode with no override. For copy-paste variants see the Slider examples.
1 <Slider defaultValue={40} size="sm" /> 2 <Slider defaultValue={50} size="md" /> 3 <Slider defaultValue={60} size="lg" /> 4 <Slider defaultValue={50} orientation="vertical" />
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
No. The Slider tracks its value in React state and handles pointer events to drag the thumb, so its source opens with a 'use client' directive and must render inside a Client Component. In the App Router, put the Slider — and the useState that holds its value — in a file that starts with 'use client', then import that file into your Server Component page. The interactive island stays scoped to the control while the rest of the route still renders on the server.
Give the Slider a name prop. Alongside the visible track it renders a hidden <input type="range" name={name}> that mirrors the current value, so it posts with any surrounding <form> — including one wired to a Next.js Server Action. On submit the value arrives in FormData under that name, and you read it server-side with formData.get('volume'), exactly as you would a native range input.
The Slider is controlled when you pass a value prop and uncontrolled when you pass defaultValue instead. Controlled means you store the value in state and update it from onChange; the component renders exactly what you hand it. Uncontrolled means the component keeps its own internal value and still calls onChange on each change. Reach for controlled when another element — a live label, a preview, a synced control — needs to read the same value.
Pass min, max, and step as numbers. They default to 0, 100, and 1. The component maps the thumb position to a value in that range and snaps it to the nearest step, so step={10} on a 0–100 range makes onChange report only 0, 10, 20, and so on. Set min and max to any bounds — min={-50} max={50} for a balance control, for example — and the thumb positions itself proportionally along the track.
Yes. The component depends only on React and the local cn utility — no Next.js API and no router — so it works anywhere React and Tailwind reach the DOM. In a Vite + React app there is no Server Component boundary to think about, and the 'use client' directive is simply ignored. Run npx drivn add slider and render it the same way, driving it with value and onChange.

