UIPackage

Range Slider

React form
Edit on GitHub

Two-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

$ npx 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