UIPackage

Slider

React form
Edit on GitHub

Single-thumb slider — pick a value within a range. Optional tick marks, step size, and inline value display.

Also available for Vue ->

Installation

$ npx shadcn@latest add https://react.uipkge.dev/r/react/slider.json

Or with the named registry: npx shadcn@latest add @uipkge-react/slider

Examples

Props

Name Type / Values Default Required
className string optional
value

Controlled value (Radix uses number[]).

number[] optional
defaultValue

Uncontrolled initial value.

number[] optional
onValueChange (value: number[]) => void optional
onValueCommit (value: number[]) => void optional
range

Enable dual-thumb range selection (affects default value when uncontrolled).

boolean optional
vertical

Vertical orientation

boolean optional
height

Height when vertical (px or css value)

string | number optional
marks

Tick marks with labels

Record<number, string | SliderMark> optional
tooltip

Show tooltip on drag/focus. Boolean or formatter function

boolean | ((value: number) => string) optional
dots

Show dots at each step

boolean optional
reverse

Reverse direction (right-to-left or bottom-to-top)

boolean optional
included

Highlight the track between thumbs/min (default true)

boolean optional
size

Size variant

'small' | 'default' optional

Schema

Type aliases from this item's source — use them to shape the data you pass in.

SliderMark
interface SliderMark {
  label: string
  style?: React.CSSProperties
}

Dependencies

Used by

Files (2)

  • components/ui/slider/Slider.tsx 9.1 kB
    'use client'
    
    import * as React from 'react'
    import * as SliderPrimitive from '@radix-ui/react-slider'
    import * as TooltipPrimitive from '@radix-ui/react-tooltip'
    import { cn } from '@/lib/utils'
    
    export interface SliderMark {
      label: string
      style?: React.CSSProperties
    }
    
    export interface SliderProps
      extends Omit<
        React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>,
        'value' | 'defaultValue' | 'inverted' | 'orientation' | 'onValueChange' | 'onValueCommit'
      > {
      className?: string
      /** Controlled value (Radix uses number[]). */
      value?: number[]
      /** Uncontrolled initial value. */
      defaultValue?: number[]
      onValueChange?: (value: number[]) => void
      onValueCommit?: (value: number[]) => void
      /** Enable dual-thumb range selection (affects default value when uncontrolled). */
      range?: boolean
      /** Vertical orientation */
      vertical?: boolean
      /** Height when vertical (px or css value) */
      height?: string | number
      /** Tick marks with labels */
      marks?: Record<number, string | SliderMark>
      /** Show tooltip on drag/focus. Boolean or formatter function */
      tooltip?: boolean | ((value: number) => string)
      /** Show dots at each step */
      dots?: boolean
      /** Reverse direction (right-to-left or bottom-to-top) */
      reverse?: boolean
      /** Highlight the track between thumbs/min (default true) */
      included?: boolean
      /** Size variant */
      size?: 'small' | 'default'
    }
    
    const Slider = React.forwardRef<React.ElementRef<typeof SliderPrimitive.Root>, SliderProps>(
      (
        {
          className,
          value,
          defaultValue,
          onValueChange,
          onValueCommit,
          min = 0,
          max = 100,
          step = 1,
          range = false,
          vertical = false,
          height,
          marks,
          tooltip = true,
          dots = false,
          reverse = false,
          included = true,
          size = 'default',
          ...props
        },
        ref,
      ) => {
        /* ── normalize value to number[] ── */
        const resolvedDefault = React.useMemo<number[]>(() => {
          if (defaultValue != null) return defaultValue
          return range ? [min, max] : [min]
        }, [defaultValue, range, min, max])
    
        // Track current values so tooltips/marks can read them in both controlled
        // and uncontrolled usage (Radix doesn't surface the value to children).
        const [internalValue, setInternalValue] = React.useState<number[]>(resolvedDefault)
        const isControlled = value !== undefined
        const currentValue = isControlled ? value! : internalValue
    
        const handleChange = React.useCallback(
          (next: number[]) => {
            if (!isControlled) setInternalValue(next)
            onValueChange?.(next)
          },
          [isControlled, onValueChange],
        )
    
        /* ── layout helpers ── */
        const orientation = vertical ? 'vertical' : 'horizontal'
        const isHorizontal = orientation === 'horizontal'
    
        const trackSize =
          size === 'small'
            ? isHorizontal
              ? 'h-1'
              : 'w-1'
            : isHorizontal
              ? 'h-1.5'
              : 'w-1.5'
    
        const thumbSize = size === 'small' ? 'size-3' : 'size-4'
    
        /* ── marks ── */
        const markList = React.useMemo(() => {
          if (!marks) return []
          const entries = Object.entries(marks).map(([key, val]) => {
            const num = Number(key)
            const label = typeof val === 'string' ? val : val.label
            const style = typeof val === 'string' ? undefined : val.style
            const pct = ((num - min) / (max - min)) * 100
            return { value: num, label, style, pct }
          })
          entries.sort((a, b) => a.value - b.value)
          return entries
        }, [marks, min, max])
    
        /* ── step dots ── */
        const dotList = React.useMemo(() => {
          if (!dots) return []
          const list: number[] = []
          const count = Math.floor((max - min) / step)
          for (let i = 0; i <= count; i++) {
            list.push(min + i * step)
          }
          return list
        }, [dots, max, min, step])
    
        /* ── tooltip ── */
        const showTooltip = tooltip !== false
        const formatTooltip = (v: number) =>
          typeof tooltip === 'function' ? tooltip(v) : String(v)
    
        const thumbClass = cn(
          'border-primary bg-background ring-ring/50 block shrink-0 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',
          thumbSize,
        )
    
        return (
          <TooltipPrimitive.Provider>
            <div
              className={cn('relative w-full', !isHorizontal && 'flex flex-col items-center')}
              style={
                !isHorizontal && height
                  ? { height: typeof height === 'number' ? `${height}px` : height }
                  : undefined
              }
            >
              <SliderPrimitive.Root
                ref={ref}
                data-uipkge=""
                data-slot="slider"
                min={min}
                max={max}
                step={step}
                value={isControlled ? value : undefined}
                defaultValue={isControlled ? undefined : resolvedDefault}
                orientation={orientation}
                inverted={reverse}
                onValueChange={handleChange}
                onValueCommit={onValueCommit}
                className={cn(
                  'relative flex touch-none select-none data-[disabled]:opacity-50',
                  isHorizontal ? 'w-full items-center' : 'h-full min-h-44 flex-col justify-center',
                  className,
                )}
                {...props}
              >
                {/* Track */}
                <SliderPrimitive.Track
                  data-uipkge=""
                  data-slot="slider-track"
                  className={cn('bg-muted relative grow overflow-hidden rounded-full', trackSize)}
                >
                  {included && (
                    <SliderPrimitive.Range
                      data-uipkge=""
                      data-slot="slider-range"
                      className={cn('bg-primary absolute', isHorizontal ? 'h-full' : 'w-full')}
                    />
                  )}
                </SliderPrimitive.Track>
    
                {/* Step dots */}
                {dotList.map((dot) => (
                  <div
                    key={dot}
                    className={cn(
                      'border-primary/40 bg-background absolute rounded-full border',
                      isHorizontal
                        ? 'top-1/2 size-1.5 -translate-y-1/2'
                        : 'left-1/2 size-1.5 -translate-x-1/2',
                      size === 'small' && 'size-1',
                    )}
                    style={
                      isHorizontal
                        ? {
                            left: `${((dot - min) / (max - min)) * 100}%`,
                            transform: 'translateX(-50%) translateY(-50%)',
                          }
                        : {
                            bottom: `${((dot - min) / (max - min)) * 100}%`,
                            transform: 'translateX(-50%) translateY(50%)',
                          }
                    }
                  />
                ))}
    
                {/* Marks */}
                {markList.length > 0 && (
                  <div
                    className={cn(
                      'pointer-events-none absolute',
                      isHorizontal ? 'top-full mt-2.5 h-5 w-full' : 'top-0 left-full ml-3 h-full w-20',
                    )}
                  >
                    {markList.map((mark) => (
                      <span
                        key={mark.value}
                        className="text-muted-foreground absolute text-xs whitespace-nowrap"
                        style={{
                          ...mark.style,
                          ...(isHorizontal
                            ? { left: `${mark.pct}%`, transform: 'translateX(-50%)' }
                            : { bottom: `${mark.pct}%`, transform: 'translateY(50%)' }),
                        }}
                      >
                        {mark.label}
                      </span>
                    ))}
                  </div>
                )}
    
                {/* Thumbs (with tooltips) */}
                {currentValue.map((thumbValue, idx) =>
                  showTooltip ? (
                    <TooltipPrimitive.Root key={idx} delayDuration={0}>
                      <TooltipPrimitive.Trigger asChild>
                        <SliderPrimitive.Thumb
                          data-uipkge=""
                          data-slot="slider-thumb"
                          className={thumbClass}
                        />
                      </TooltipPrimitive.Trigger>
                      <TooltipPrimitive.Portal>
                        <TooltipPrimitive.Content
                          side={isHorizontal ? 'top' : 'right'}
                          sideOffset={4}
                          className="bg-foreground text-background z-50 w-fit rounded-md px-2 py-1 text-xs"
                        >
                          {formatTooltip(thumbValue ?? 0)}
                          <TooltipPrimitive.Arrow className="bg-foreground fill-foreground size-2.5 rotate-45 rounded-[2px]" />
                        </TooltipPrimitive.Content>
                      </TooltipPrimitive.Portal>
                    </TooltipPrimitive.Root>
                  ) : (
                    <SliderPrimitive.Thumb
                      key={idx}
                      data-uipkge=""
                      data-slot="slider-thumb"
                      className={thumbClass}
                    />
                  ),
                )}
              </SliderPrimitive.Root>
            </div>
          </TooltipPrimitive.Provider>
        )
      },
    )
    Slider.displayName = 'Slider'
    
    export { Slider }
  • components/ui/slider/index.ts 0.1 kB
    export { Slider, type SliderProps, type SliderMark } from './Slider'

Raw manifest: https://react.uipkge.dev/r/react/slider.json