rapidUI

Slider

Range slider with one or more thumbs, built on Base UI Slider.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/slider.json

Installs to @/components/ui/slider.

Manual installation

Install the dependencies:

pnpm add @base-ui/react@^1.8.0

Copy the source into your project:

components/ui/slider.tsx
'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 as value
  • onValueCommitted - Called when the user finishes dragging or releases a key
  • minStepsBetweenValues - Minimum number of steps between thumbs in a range
  • aria-labelledby - The id of the visible label; it names every thumb
  • aria-label - Accessible name for the thumb when there is no visible label
  • getAriaLabel - (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 step
    • Page Up/Down: Large increments
    • Home/End: Min/max values
  • Announces value changes to screen readers
  • Maintains focus management
  • Provides visual feedback for interaction states

Best Practices

  1. Value Range:

    • Set appropriate min/max values
    • Choose logical step increments
    • Consider the precision needed
  2. Visual Feedback:

    • Use clear track and thumb styles
    • Provide hover and focus states
    • Show value labels when helpful
  3. Accessibility:

    • Include descriptive labels
    • Maintain sufficient touch targets
    • Ensure keyboard navigation
  4. Units and Labels:

    • Use clear, concise units
    • Show min/max when context is needed
    • Add descriptive labels for context
  5. Interaction:

    • Consider step size for precision
    • Implement smooth transitions
    • Handle edge cases gracefully
  • Input
  • Number Input
  • Progress