Slider React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React slider for a number or a range: any number of thumbs, labelled marks, a value bubble, vertical mode, tones and finger-sized thumbs.
Drag a thumb or tap anywhere on the track to jump there; the thumb grows a little under the finger and a bubble with the formatted value rides above it, never under the finger. With a mouse the thumb shows a soft halo and its bubble on hover. Tab to a thumb for a focus ring and the bubble, then use the arrow keys for one step, Shift+arrow or PageUp / PageDown for ten, and Home / End for the ends; a range has one tab stop per thumb and they never cross. Tap a mark's label (New, Like new, 25 km) to send the nearest thumb there. On a phone each thumb has a 44px target, the whole row answers to a tap, and a sideways drag never scrolls the page. The playground filter counts the matching listings live and Reset puts both sliders back.
Slider is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add slider, together with UIProvider, Field, useControllableState, useLocale, Motion presets and 2 more and the npm packages @radix-ui/react-slider and motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add sliderSlider is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import Slider from @/components/ui/slider.
import { useState } from "react"
import { Slider } from "@/components/ui/slider"
const usd = { style: "currency", currency: "USD", maximumFractionDigits: 0 } as const
export function PriceFilter({ onApply }: { onApply: (range: number[]) => void }) {
const [price, setPrice] = useState([500, 2000])
return (
<div className="flex flex-col gap-6">
<Slider
label="Price"
value={price}
onValueChange={setPrice}
onValueCommit={onApply}
min={0}
max={5000}
step={50}
minStepsBetweenThumbs={2}
locale="en-US"
format={usd}
showValueLabel
/>
<Slider
label="Search radius"
defaultValue={[25]}
min={5}
max={100}
step={5}
suffix="km"
marks={[5, 25, 50, 75, 100].map((value) => ({ value, label: `${value} km` }))}
showValueLabel
/>
</div>
)
}Props
labeldefault""ReactNodeThe field label. One thumb is labelled by it directly (a click on it focuses the thumb); two or more thumbs are named as a group by it and each thumb gets its own name from thumbLabels.
descriptiondefault""ReactNodeHelper text under the slider, linked to every thumb with aria-describedby. The error takes its place while it shows.
errordefault""ReactNodeReplaces the description with the animated error, turns the range red and sets aria-invalid on the thumbs.
requireddefaultfalsebooleanThe required mark after the label. A slider always holds a value, so it only marks the field.
invaliddefaultfalsebooleanThe red range and aria-invalid without a message, for forms that show the error elsewhere.
disableddefaultfalsebooleanDims to 50%, takes the thumbs out of the tab order and ignores every drag and tap. A disabled FieldGroup disables it too.
readOnlydefaultfalsebooleanShows the value and keeps the thumbs focusable (so the value can be read), but ignores drags, taps and keys.
mindefault0numberThe lowest value. The playground shows its dollar sample (0 to 5,000, step 50) while min, max and step are all at their defaults.
maxdefault100numberThe highest value. A max at or below min falls back to min + step.
stepdefault1numberThe distance between values, for drags and arrow keys. A step that does not divide the range still reaches max: a drag past the middle of the last gap lands on it.
minStepsBetweenThumbsdefault0numberHow many steps a range keeps between its thumbs. The playground names the gap under the slider.
sizedefault"md""sm" | "md" | "lg"Track 4 / 6 / 8px, thumb 16 / 20 / 24px, and the value label's type. Every thumb gets a 44px hit area on touch screens.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The range fill and the hover halo. neutral is black in light mode and white in dark mode.
colordefault""stringAny CSS color for the range; replaces the tone. Marks inside the range pick a readable dot color on their own.
marksdefaultfalseboolean | number[] | SliderMark[]Dots on the track. true puts one on every step when there are 20 steps or fewer, else five evenly spaced; number[] places them; SliderMark[] ({ value, label }) adds a label under each. Marks outside min..max are ignored.
showMarkLabelsdefaultfalsebooleanWrites the formatted value under every mark that has no label of its own. The label under a thumb's value turns to the text color.
showValuedefault"auto""auto" | "always" | "never"The value bubble above each thumb (beside it when vertical). auto shows it while dragging, on hover and on keyboard focus; always keeps it up and, under a label, makes room for it so it never covers the label row.
showValueLabeldefaultfalsebooleanThe formatted value at the end of the label row: "$1,250", or both ends of a range joined by an en dash, with a shared unit written once after the second number. Without a label it gets a row of its own.
orientationdefault"horizontal""horizontal" | "vertical"vertical stands 180px tall with the mark labels beside the track and the bubble to the side. Size it with classNames.box.
inverteddefaultfalsebooleanFills from the other end: right to left, or top to bottom when vertical. Arrow keys follow.
localedefault""stringLocale for every number shown (any BCP 47 tag). Empty uses the nearest LocaleProvider, then the page's language (en-US on the server). The demo passes en-US while it is empty.
prefixdefault""stringA simple unit before the number when there is no format: "~", "$". Letters get a space ("USD 50"), symbols do not ("$50"). The demo drops its dollar format while one is set.
suffixdefault""stringA simple unit after the number when there is no format: "km", "%", "m²". Letters get a non-breaking space ("50 km"), symbols do not ("50%").
formatIntl.NumberFormatOptionsIntl options for the bubble, the value label and the mark labels. Dollars: { style: "currency", currency: "USD", maximumFractionDigits: 0 }. Without it numbers keep the step's decimals.
formatValue(value: number) => ReactNodeReplaces the formatted number everywhere it shows, e.g. a word per step (New, Like new, Good, Worn). A string result is also what screen readers hear.
formatValueLabel(values: number[]) => ReactNodeReplaces the whole value label, e.g. "Any" when the range covers everything.
thumbLabelsstring[]Each thumb's aria-label. Two thumbs default to "Minimum" and "Maximum", one thumb to the field label (or aria-label), three or more to "Value 1 of 3"…
valuenumber[]The controlled values, one per thumb. A value outside min..max is drawn at the edge and never corrected behind your back.
defaultValuenumber[]The uncontrolled start, clamped into the range once. Defaults to the middle of min..max ([50] for 0..100). Two numbers make a range.
onValueChange(value: number[]) => voidEvery change while dragging, tapping or pressing keys.
onValueCommit(value: number[]) => voidWhen a drag, a tap or a key press ends: the moment to fetch results or save.
namedefault""stringForm field name. Inside a <form> each thumb submits a hidden input (name[] for ranges), and a form reset puts the thumbs back.
idstringThe thumb's id (the group's for ranges). Defaults to useId(); the label points at it.
fieldPropsPartial<FieldProps>Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel.
classNamesPartial<Record<"box" | "root" | "track" | "range" | "thumb" | "mark" | "markLabel" | "value" | "valueLabel" | "field" | "label" | "description" | "error", string>>Extra classes for the inner parts: box (the slider and its mark labels; size a vertical slider here), the radix root, track, range, thumbs, mark dots and labels, the bubble, the value label and the field parts.
classNamestringClasses for the outermost element: the Field root when the slider has a label, description or error, else the slider's box.
Examples
Value always on
showValue="always" keeps the bubble up, like a volume control. The icons jump to the ends.
Range with a minimum gap
minStepsBetweenThumbs keeps the two ends apart; the shared unit is written once in the value label.
Labelled scale
SliderMark labels under the track and formatValue for the bubble. Tap a word to jump there.
Vertical
orientation="vertical" with mark labels beside the track, driving a picture of the block.
Sizes, tones and a custom color
Track 4 / 6 / 8px and thumb 16 / 20 / 24px; any tone or CSS color for the range, marks adapt.
Two locales
The same range through Intl: 500 – 2.000 lei in ro-RO next to $500 – $2,000 in en-US.
Invalid
The range turns red and the error slides in; drag the ends apart and it slides away.
Price histogram
A bare range under a histogram that shares its tone, the classic marketplace price filter.
Dependencies
Files
- components/ui/slider.tsx
FAQ
Is the React Slider component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Slider?
Run npx wingo-ui@latest add slider. It also adds the Wingo UI items it builds on (UIProvider, Field, useControllableState, useLocale, Motion presets and 2 more) and the npm packages @radix-ui/react-slider and motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/slider.
How does Slider behave on phones?
On a phone each thumb has a 44px target, the whole row answers to a tap, and a sideways drag never scrolls the page.
See also
Used in
More React input components
Articles using this component
- Tailwind Touch Manipulation: touch-action for Every Control
Mobile UI in React9 min read