Range Slider
React formTwo-thumb range slider for "between X and Y" inputs — price filters, age ranges, time windows. Built on @radix-ui/react-slider with proper keyboard handling and aria-valuetext.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://react.uipkge.dev/r/react/range-slider.json$ npx shadcn@latest add https://react.uipkge.dev/r/react/range-slider.json$ yarn dlx shadcn@latest add https://react.uipkge.dev/r/react/range-slider.json$ bunx shadcn@latest add https://react.uipkge.dev/r/react/range-slider.json
Or with the named registry:
npx shadcn@latest add @uipkge-react/range-slider
Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
className | string | — | optional |
value The controlled value of the range slider. | [number, number] | — | optional |
defaultValue The value of the range slider when initially rendered. | [number, number] | — | optional |
onValueChange | (value: [number, number]) => void | — | optional |
disabled When `true`, prevents the user from interacting with the range slider | boolean | — | optional |
min Minimum value | number | — | optional |
max Maximum value | number | — | optional |
step Step value | number | — | optional |
label Label for the range slider | string | — | optional |
hint Hint text for the range slider | string | — | optional |
errorMessages Error messages to display | string | string[] | — | optional |
error Whether to show error state | boolean | — | optional |
color Custom color for the track fill | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | string | — | optional |
thumbSize Thumb size | 'sm' | 'md' | 'lg' | — | optional |
trackHeight Track height | 'sm' | 'md' | 'lg' | — | optional |
showTicks Show ticks | boolean | — | optional |
tickInterval Tick interval | number | — | optional |
thumbLabel Show thumb labels | boolean | — | optional |
thumbLabelFormat Format thumb label | (value: number) => string | — | optional |
Dependencies
Files (2)
-
components/ui/range-slider/RangeSlider.tsx 7.5 kB
'use client' import * as React from 'react' import * as SliderPrimitive from '@radix-ui/react-slider' import { cn } from '@/lib/utils' export interface RangeSliderProps extends Omit< React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>, 'value' | 'defaultValue' | 'onValueChange' | 'min' | 'max' | 'step' > { className?: string /** The controlled value of the range slider. */ value?: [number, number] /** The value of the range slider when initially rendered. */ defaultValue?: [number, number] onValueChange?: (value: [number, number]) => void /** When `true`, prevents the user from interacting with the range slider */ disabled?: boolean /** Minimum value */ min?: number /** Maximum value */ max?: number /** Step value */ step?: number /** Label for the range slider */ label?: string /** Hint text for the range slider */ hint?: string /** Error messages to display */ errorMessages?: string | string[] /** Whether to show error state */ error?: boolean /** Custom color for the track fill */ color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | string /** Thumb size */ thumbSize?: 'sm' | 'md' | 'lg' /** Track height */ trackHeight?: 'sm' | 'md' | 'lg' /** Show ticks */ showTicks?: boolean /** Tick interval */ tickInterval?: number /** Show thumb labels */ thumbLabel?: boolean /** Format thumb label */ thumbLabelFormat?: (value: number) => string } // Color classes const colorClasses: Record<string, string> = { primary: 'bg-primary', secondary: 'bg-secondary', success: 'bg-[var(--success)]', warning: 'bg-[var(--warning)]', error: 'bg-destructive', info: 'bg-[var(--info)]', } // Track height classes const trackHeightClasses = { sm: 'h-1', md: 'h-1.5', lg: 'h-2', } // Thumb size classes const thumbSizeClasses = { sm: 'size-3', md: 'size-4', lg: 'size-5', } const RangeSlider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, RangeSliderProps>( ( { className, value, defaultValue, onValueChange, disabled, min = 0, max = 100, step = 1, label, hint, errorMessages, error, color = 'primary', thumbSize = 'md', trackHeight = 'md', showTicks, tickInterval, thumbLabel, thumbLabelFormat, ...props }, ref, ) => { const isControlled = value !== undefined const [internalValue, setInternalValue] = React.useState<[number, number]>( defaultValue ?? [min, max], ) const currentValue = isControlled ? value! : internalValue const handleChange = React.useCallback( (next: number[]) => { const tuple = [next[0], next[1]] as [number, number] if (!isControlled) setInternalValue(tuple) onValueChange?.(tuple) }, [isControlled, onValueChange], ) // Generate ticks const ticks = React.useMemo(() => { if (!showTicks || !tickInterval) return [] const result: number[] = [] for (let i = min; i <= max; i += tickInterval) { result.push(i) } return result }, [showTicks, tickInterval, min, max]) // Build error state const hasError = React.useMemo(() => { if (error) return true if (errorMessages && (typeof errorMessages === 'string' ? errorMessages : errorMessages.length > 0)) return true return false }, [error, errorMessages]) const thumbClass = cn( 'border-primary ring-ring/50 bg-background block rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50', thumbSizeClasses[thumbSize], hasError && 'border-destructive', ) return ( <div className="flex flex-col gap-2"> {label && <label className="text-sm font-medium">{label}</label>} {hint && !hasError && <p className="text-muted-foreground text-xs">{hint}</p>} <div className="flex items-center gap-4"> {/* Min value display */} <div className="text-muted-foreground min-w-[3rem] text-sm">{currentValue?.[0] ?? min}</div> <SliderPrimitive.Root ref={ref} data-slot="range-slider" disabled={disabled} min={min} max={max} step={step} value={isControlled ? value : undefined} defaultValue={isControlled ? undefined : (defaultValue ?? [min, max])} onValueChange={handleChange} className={cn('relative flex w-full touch-none items-center select-none', className)} {...props} > <SliderPrimitive.Track data-uipkge="" data-slot="slider-track" className={cn( 'bg-muted relative w-full overflow-hidden rounded-full', trackHeightClasses[trackHeight], )} > <SliderPrimitive.Range data-uipkge="" data-slot="slider-range" className={cn('absolute h-full', colorClasses[color as string] || colorClasses.primary)} /> </SliderPrimitive.Track> {/* Ticks */} {showTicks && ticks.length > 0 && ( <div className="pointer-events-none absolute top-1/2 right-0 left-0 flex -translate-y-1/2 justify-between"> {ticks.map((tick) => ( <div key={tick} className="bg-muted-foreground/30 h-2 w-0.5 rounded-full" /> ))} </div> )} {/* Start Thumb (Min) */} <SliderPrimitive.Thumb data-uipkge="" data-slot="slider-thumb" className={thumbClass}> {thumbLabel && ( <span className="bg-background absolute -top-6 left-1/2 -translate-x-1/2 rounded px-1 text-xs whitespace-nowrap"> {thumbLabelFormat ? thumbLabelFormat(currentValue?.[0] ?? 0) : (currentValue?.[0] ?? 0)} </span> )} </SliderPrimitive.Thumb> {/* End Thumb (Max) */} <SliderPrimitive.Thumb data-uipkge="" data-slot="slider-thumb" className={thumbClass}> {thumbLabel && ( <span className="bg-background absolute -top-6 left-1/2 -translate-x-1/2 rounded px-1 text-xs whitespace-nowrap"> {thumbLabelFormat ? thumbLabelFormat(currentValue?.[1] ?? 0) : (currentValue?.[1] ?? 0)} </span> )} </SliderPrimitive.Thumb> </SliderPrimitive.Root> {/* Max value display */} <div className="text-muted-foreground min-w-[3rem] text-sm">{currentValue?.[1] ?? max}</div> </div> {/* Tick labels */} {showTicks && ticks.length > 0 && ( <div className="text-muted-foreground flex justify-between px-1 text-xs"> <span>{min}</span> <span>{max}</span> </div> )} {hasError && ( <div className="flex flex-col gap-0.5"> {typeof errorMessages === 'string' ? ( <p className="text-destructive text-xs">{errorMessages}</p> ) : ( errorMessages?.map((msg, i) => ( <p key={i} className="text-destructive text-xs"> {msg} </p> )) )} </div> )} </div> ) }, ) RangeSlider.displayName = 'RangeSlider' export { RangeSlider } -
components/ui/range-slider/index.ts 0.1 kB
export { RangeSlider, type RangeSliderProps } from './RangeSlider'
Raw manifest: https://react.uipkge.dev/r/react/range-slider.json