Sliders

Sliders let users make selections from a range of values

Usage

Sliders allow users to view and select a value (or range) along a track. They’re ideal for adjusting settings such as volume and brightness, or for applying image filters.

Instalation

Run the following command:

npm i @radix-ui/react-slider

Copy and paste the following code into your project.

'use client'

import * as React from 'react'
import * as SliderPrimitive from '@radix-ui/react-slider'

import { cn } from '@/lib/utils'

function convertValueToPercentage(
  index: number,
  min: number = 0,
  max: number = 100,
  step: number
) {
  const maxSteps = max - min
  const percentPerStep = 100 / maxSteps
  const percentage = parseFloat((percentPerStep * index * step).toFixed(1))
  const offset = (2 * step ** 2) / maxSteps

  return { percent: Math.min(100, Math.max(0, percentage)), offset }
}

const SliderThumb = React.forwardRef<
  React.ElementRef<typeof SliderPrimitive.Thumb>,
  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Thumb>
>(({ className, ...props }, ref) => (
  <SliderPrimitive.Thumb
    ref={ref}
    className={cn(
      'group/thumb peer/thumb relative block h-5 w-5 cursor-pointer rounded-lg bg-primary shadow-sm outline-none transition-colors before:absolute before:inset-0 before:z-[-1] before:scale-0 before:rounded-full before:bg-primary/8 before:transition-transform hover:before:scale-[2] focus-visible:before:scale-[2] data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:bg-onSurface/38 data-[disabled]:shadow-none data-[disabled]:before:scale-100 data-[disabled]:before:bg-background',
      className
    )}
    {...props}
  />
))
SliderThumb.displayName = SliderPrimitive.Thumb.displayName

const getTickMarkerColor = (
  value: number[],
  step: number,
  index: number,
  min: number = 0,
  max: number = 100
) => {
  const position = convertValueToPercentage(index, min, max, step).percent

  if (value.length > 1) {
    if (position >= value[0] && position <= value[value.length - 1]) {
      return 'bg-onPrimary/38'
    }

    return 'bg-onSurface/38'
  }

  if (value[0] >= position) {
    return 'bg-onPrimary/38'
  }
  return 'bg-onSurface/38'
}

const SliderRoot = React.forwardRef<
  React.ElementRef<typeof SliderPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {
    discrete?: boolean
  }
>(
  (
    { className, children, step, discrete, value, orientation, ...props },
    ref
  ) => {
    const [valueState, setValueState] = React.useState(
      value ?? props?.defaultValue ?? [0]
    )

    const handleValueChange = (value: number[]) => {
      setValueState(value)
      props.onValueChange?.(value)
    }

    return (
      <SliderPrimitive.Root
        ref={ref}
        className={cn(
          'group relative flex w-full min-w-[200px] touch-none select-none items-center',
          className
        )}
        step={step}
        value={value ?? valueState}
        onValueChange={handleValueChange}
        orientation="horizontal"
        {...props}
      >
        <SliderPrimitive.Track
          className={cn(
            'relative z-0 h-1 w-full grow cursor-pointer overflow-hidden'
          )}
        >
          <span className="absolute inset-0 z-[-1] mx-[6px] rounded-full bg-surfaceContainerHighest data-[disabled]:bg-onSurface/12" />
          <SliderPrimitive.Range className="absolute z-0 ml-[6px] h-full rounded-full bg-primary data-[disabled]:bg-onSurface/38" />
          {step &&
            step > 1 &&
            discrete &&
            new Array(
              Math.ceil(((props?.max ?? 100) - (props?.min ?? 0)) / step) + 1
            )
              .fill(0)
              .map((_, i) => {
                return (
                  <SliderTickMarker
                    key={i}
                    index={i}
                    pos={convertValueToPercentage(
                      i,
                      props.min,
                      props.max,
                      step
                    )}
                    className={getTickMarkerColor(
                      value ?? valueState,
                      step,
                      i,
                      props.min,
                      props.max
                    )}
                  />
                )
              })}
        </SliderPrimitive.Track>
        {children}
      </SliderPrimitive.Root>
    )
  }
)
SliderRoot.displayName = SliderPrimitive.Root.displayName

const SliderValueLabel = React.forwardRef<
  HTMLSpanElement,
  React.HTMLAttributes<HTMLSpanElement>
>(({ className, ...props }, ref) => (
  <span
    ref={ref}
    className={cn(
      'absolute -left-1 top-0 z-0 flex h-7 w-7 origin-bottom -translate-y-10 scale-0 items-center justify-center rounded-lg bg-primary text-label-md text-onPrimary transition-transform before:absolute before:z-[-1] before:h-[17px] before:w-[17px] before:translate-y-[45%] before:rotate-45 before:rounded-[2px] before:bg-primary group-hover/thumb:scale-100 ',
      className
    )}
    {...props}
  />
))
SliderValueLabel.displayName = 'SliderValueLabel'

const SliderTickMarker = React.forwardRef<
  HTMLSpanElement,
  React.HTMLAttributes<HTMLSpanElement> & {
    index: number
    pos: {
      percent: number
      offset: number
    }
  }
>(({ className, index, pos, ...props }, ref) => (
  <span
    ref={ref}
    className={cn(
      'absolute mt-[1px] h-0.5 w-0.5 translate-x-[-50%] rounded-sm bg-onSurfaceVariant/38',
      className
    )}
    style={{
      left: `calc(${pos.percent}% + ${10 - index * pos.offset}px)`,
    }}
    {...props}
  />
))

const Slider = Object.assign(SliderRoot, {
  Thumb: SliderThumb,
  ValueLabel: SliderValueLabel,
  TickMarker: SliderTickMarker,
})

export { Slider, SliderThumb, SliderRoot, SliderValueLabel, SliderTickMarker }

Update the import paths to match your project setup.

Examples

Continuous

Allows users to select a value along a subjective range.

import { Slider } from '@/components/ui/slider'

export const ContinuousSlider = () => {
  return (
    <Slider defaultValue={[50]} max={100} step={1} className="max-w-[320px]">
      <Slider.Thumb />
    </Slider>
  )
}

Discrete

Allows users to select a specific value from a predetermined range.

import { useState } from 'react'

import { Slider } from '@/components/ui/slider'

export const DiscreteSlider = () => {
  const [value, setValue] = useState([50])

  return (
    <Slider
      value={value}
      onValueChange={setValue}
      step={10}
      className="max-w-[320px]"
      discrete
    >
      <Slider.Thumb>
        <Slider.ValueLabel>{value[0]}</Slider.ValueLabel>
      </Slider.Thumb>
    </Slider>
  )
}

Range

When sliders have two handles, they indicate the minimum and maximum values in a range.

import { useState } from 'react'

import { Slider } from '@/components/ui/slider'

export const RangeSlider = () => {
  const [rangeValue, setRangeValue] = useState([30, 60])

  return (
    <Slider
      value={rangeValue}
      onValueChange={setRangeValue}
      step={10}
      className="max-w-[320px]"
      discrete
    >
      <Slider.Thumb>
        <Slider.ValueLabel>{rangeValue[0]}</Slider.ValueLabel>
      </Slider.Thumb>
      <Slider.Thumb>
        <Slider.ValueLabel>{rangeValue[1]}</Slider.ValueLabel>
      </Slider.Thumb>
    </Slider>
  )
}