Installation
pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/slider.jsonInstalls to @/components/ui/slider.
Manual installation
Install the dependencies:
pnpm add @base-ui/react@^1.8.0Copy the source into your project:
'use client'
import { Slider as SliderPrimitive } from '@base-ui/react/slider'
import { cn } from '@/lib/utils'
/** One thumb per value: an array gives one thumb per entry, a single number one thumb, and no value a range */
function getThumbCount(value: number | readonly number[] | undefined) {
if (Array.isArray(value)) return value.length
return typeof value === 'number' ? 1 : 2
}
function Slider<Value extends number | readonly number[]>({
className,
defaultValue,
value,
min = 0,
max = 100,
'aria-label': ariaLabel,
getAriaLabel,
getAriaValueText,
...props
}: SliderPrimitive.Root.Props<Value> &
Pick<SliderPrimitive.Thumb.Props, 'getAriaLabel' | 'getAriaValueText'> & {
/** Accessible name for the thumb(s). For a labelled field, pass `aria-labelledby` with the label's id instead. */
'aria-label'?: string
}) {
const thumbCount = getThumbCount(value ?? defaultValue)
return (
<SliderPrimitive.Root
className={cn('data-horizontal:w-full data-vertical:h-full', className)}
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
thumbAlignment="edge"
{...props}
>
<SliderPrimitive.Control className="relative flex w-full touch-none items-center select-none data-disabled:cursor-not-allowed data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col">
<SliderPrimitive.Track
data-slot="slider-track"
className="relative grow overflow-hidden rounded-full bg-secondary select-none data-horizontal:h-1 data-horizontal:w-full data-vertical:h-full data-vertical:w-1"
>
<SliderPrimitive.Indicator
data-slot="slider-range"
className="bg-primary select-none data-horizontal:h-full data-vertical:w-full"
/>
</SliderPrimitive.Track>
{Array.from({ length: thumbCount }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
// biome-ignore lint/suspicious/noArrayIndexKey: thumbs are identified by their position
key={index}
index={index}
// The thumbs carry role="slider", so the accessible name and value text go on them, not on the root
aria-label={ariaLabel}
getAriaLabel={getAriaLabel}
getAriaValueText={getAriaValueText}
className="relative block size-3 shrink-0 rounded-full border border-primary bg-background ring-ring/50 transition-[color,box-shadow] select-none after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:ring-ring focus-visible:outline-hidden active:ring-3 has-focus-visible:ring-3 has-focus-visible:ring-ring data-disabled:pointer-events-none data-disabled:opacity-50"
/>
))}
</SliderPrimitive.Control>
</SliderPrimitive.Root>
)
}
export { Slider }A flexible and accessible slider component that allows users to select numeric values from a range. Built on top of Base UI Slider, it follows WAI-ARIA design patterns and provides a rich set of features for numeric input.
Features
- Single value, range and multiple-thumb selection (one thumb per value)
- Horizontal and vertical orientation
- Customizable step values
- Controlled (
value/onValueChange) and uncontrolled (defaultValue) usage - Keyboard navigation and screen reader support
- Disabled state support
Usage
import { Slider } from '@/components/ui/slider'
import { Label } from '@/components/ui/label'
export function MyComponent() {
const [value, setValue] = React.useState([75])
return (
<div className="space-y-8">
{/* Simple slider */}
<Slider defaultValue={[50]} min={0} max={100} step={1} aria-label="Volume" />
{/* Range slider, one name per thumb */}
<Slider
defaultValue={[25, 75]}
getAriaLabel={(index) => (index === 0 ? 'Minimum price' : 'Maximum price')}
/>
{/* Vertical */}
<Slider defaultValue={[50]} orientation="vertical" aria-label="Temperature" />
{/* Controlled, labelled by a visible label, with value text for screen readers */}
<div className="grid gap-3">
<Label id="distance-label">Maximum distance to school</Label>
<span className="text-sm text-muted-foreground">{value[0]} km</span>
<Slider
aria-labelledby="distance-label"
getAriaValueText={(_, v) => `${v} km`}
value={value}
onValueChange={setValue}
/>
</div>
</div>
)
}The slider has no built-in unit or min/max labels — compose them around it as shown in the With Labels example.
The role="slider" elements are the thumbs, so a label with htmlFor cannot name them. Give the visible label an
id and pass it as aria-labelledby, or pass aria-label.
Examples
Default
Range Selection
Two values give two thumbs. Here a card filters municipal building plots by size, with minStepsBetweenValues keeping the handles apart.
Multiple Thumbs
Any number of thumbs – one per value. Three noise thresholds for a construction site are summarised as badges below the track.
Vertical
Set orientation="vertical" for mixer-style controls, like room temperatures in a community centre.
Controlled
Use value and onValueChange to drive other parts of the screen. Adjusting the weekly home help hours updates the estimated visits and fee.
Disabled State
A locked assessment after a decision has been made. Setting data-disabled="true" on the Field dims its label too.
Custom Steps
A five-step satisfaction scale (min={1}, max={5}, step={1}) with a word under each step; the current one is highlighted.
With Labels
Units and min/max labels composed around the slider in a procurement form.
Props
defaultValue- Initial value(s) of the slider (a number for one thumb, or an array with one entry per thumb)min- Minimum value (number)max- Maximum value (number)step- Step increment (number)disabled- Disabled state (boolean)value/onValueChange- Controlled value(s);onValueChange(value, eventDetails)gets the same shape asvalueonValueCommitted- Called when the user finishes dragging or releases a keyminStepsBetweenValues- Minimum number of steps between thumbs in a rangearia-labelledby- The id of the visible label; it names every thumbaria-label- Accessible name for the thumb when there is no visible labelgetAriaLabel-(index) => string, a name per thumb for ranges, e.g. "Minimum plot size"getAriaValueText-(formattedValue, value, index) => string, value text with units, e.g. "3 km"orientation- Layout orientation ('horizontal' | 'vertical')
All other Base UI Slider.Root props are supported. min defaults to 0 and max to 100.
Accessibility
The Slider component follows WAI-ARIA guidelines for sliders:
- Uses proper ARIA roles (
slider,group) - Supports keyboard navigation:
Arrow keys: Increment/decrement by stepPage Up/Down: Large incrementsHome/End: Min/max values
- Announces value changes to screen readers
- Maintains focus management
- Provides visual feedback for interaction states
Best Practices
-
Value Range:
- Set appropriate min/max values
- Choose logical step increments
- Consider the precision needed
-
Visual Feedback:
- Use clear track and thumb styles
- Provide hover and focus states
- Show value labels when helpful
-
Accessibility:
- Include descriptive labels
- Maintain sufficient touch targets
- Ensure keyboard navigation
-
Units and Labels:
- Use clear, concise units
- Show min/max when context is needed
- Add descriptive labels for context
-
Interaction:
- Consider step size for precision
- Implement smooth transitions
- Handle edge cases gracefully
Related Components
- Input
- Number Input
- Progress