Slider

A slider component based on the native range input

Source Code

"use client";
 
import { cva } from "@/lib/utils";
 
interface SliderProps
  extends Omit<React.ComponentPropsWithRef<"input">, "onChange"> {
  min?: number;
  max?: number;
  step?: number;
  defaultValue: number;
  onChange?: (value: number) => void;
  orientation?: "horizontal" | "vertical";
}
 
const containerStyle = cva({
  base: [
    // CSS variables
    "[--track-thickness:--spacing(2)]",
    "[--thumb-size:--spacing(4)]",
 
    "inline-flex relative",
  ],
  variants: {
    variant: {
      horizontal: "items-center w-56 h-(--track-thickness)",
      vertical: "justify-center w-(--track-thickness) h-56",
    },
    disabled: {
      true: "opacity-60 pointer-events-none",
      false: "",
    },
  },
  defaultVariants: {
    variant: "horizontal",
    disabled: false,
  },
});
 
const inputStyle = cva({
  base: [
    "absolute cursor-pointer appearance-none rounded-full outline-none",
 
    // thumb
    // webkit
    "[&::-webkit-slider-thumb]:appearance-none",
    "[&::-webkit-slider-thumb]:transition-transform",
    "[&::-webkit-slider-thumb]:h-(--thumb-size) [&::-webkit-slider-thumb]:w-(--thumb-size) [&::-webkit-slider-thumb]:bg-foreground",
    "[&::-webkit-slider-thumb]:rounded-full z-[10]",
    // firefox
    "[&::-moz-range-thumb]:border-none",
    "[&::-moz-range-thumb]:transition-transform",
    "[&::-moz-range-thumb]:h-(--thumb-size) [&::-moz-range-thumb]:w-(--thumb-size) [&::-moz-range-thumb]:bg-foreground",
    "[&::-moz-range-thumb]:rounded-full",
 
    // thumb hover
    // webkit
    "[&::-webkit-slider-thumb:hover]:scale-130",
    // firefox
    "[&::-moz-range-thumb:hover]:scale-130",
 
    // thumb active
    // webkit
    "[&:active::-webkit-slider-thumb]:ring-5",
    "[&:active::-webkit-slider-thumb]:ring-foreground/40",
    // firefox
    "[&:active::-moz-range-thumb]:ring-5",
    "[&:active::-moz-range-thumb]:ring-foreground/40",
 
    // thumb focus
    // webkit (chrome, safari)
    "[&:focus::-webkit-slider-thumb]:ring-5",
    "[&:focus::-webkit-slider-thumb]:ring-foreground/40",
    // firefox
    "[&:focus::-moz-range-thumb]:ring-5",
    "[&:focus::-moz-range-thumb]:ring-foreground/40",
  ],
  variants: {
    variant: {
      horizontal: [
        "w-full h-(--track-thickness)",
 
        // thumb
        // webkit
        "[&::-webkit-slider-thumb]:-mt-[calc(var(--thumb-size)-var(--track-thickness))/2]",
        // firefox
        "[&::-moz-range-thumb]:-mt-[calc(var(--thumb-size)-var(--track-thickness))/2]",
 
        // track
        // webkit
        "[&::-webkit-slider-runnable-track]:h-(--track-thickness)",
        // firefox
        "[&::-moz-range-track]:h-(--track-thickness)",
      ],
      vertical: [
        "w-(--track-thickness) h-full [writing-mode:vertical-lr] [direction:rtl]",
 
        // thumb
        // webkit
        "[&::-webkit-slider-thumb]:-ml-[calc(var(--thumb-size)-var(--track-thickness))/2]",
        // firefox
        "[&::-moz-range-thumb]:-ml-[calc(var(--thumb-size)-var(--track-thickness))/2]",
 
        // track
        // webkit
        "[&::-webkit-slider-runnable-track]:w-(--track-thickness)",
        // firefox
        "[&::-moz-range-track]:w-(--track-thickness)",
      ],
    },
  },
  defaultVariants: {
    variant: "horizontal",
  },
});
 
const backgroundTrackStyle = cva({
  base: ["absolute bg-foreground-secondary/40 rounded-full"],
  variants: {
    variant: {
      horizontal:
        "left-[calc(var(--thumb-size)/2)] w-[calc(100%-var(--thumb-size))] h-(--track-thickness)",
      vertical:
        "bottom-[calc(var(--thumb-size)/2)] w-(--track-thickness) h-[calc(100%-var(--thumb-size))]",
    },
  },
  defaultVariants: {
    variant: "horizontal",
  },
});
 
const progressTrackStyle = cva({
  base: ["absolute bg-foreground/90 rounded-full z-[5]"],
  variants: {
    variant: {
      horizontal: "left-[calc(var(--thumb-size)/2)]",
      vertical: "bottom-[calc(var(--thumb-size)/2)]",
    },
  },
  defaultVariants: {
    variant: "horizontal",
  },
});
 
const Slider = ({
  min = 0,
  max = 100,
  step = 1,
  defaultValue,
  onChange,
  orientation = "horizontal",
  disabled = false,
  className,
  ...props
}: SliderProps) => {
  const calcProgressTrackFactor = (value: number) => value / max;
 
  return (
    <div
      className={containerStyle({ variant: orientation, disabled, className })}
      style={{
        "--progress-track-factor": `${calcProgressTrackFactor(defaultValue)}`,
      }}
    >
      <div className={backgroundTrackStyle({ variant: orientation })} />
      <div
        className={progressTrackStyle({ variant: orientation })}
        style={{
          [orientation === "horizontal" ? "width" : "height"]:
            `calc(var(--progress-track-factor)*100% - var(--thumb-size) * var(--progress-track-factor))`,
          [orientation === "horizontal" ? "height" : "width"]:
            `var(--track-thickness)`,
        }}
      />
      <input
        type="range"
        min={min}
        max={max}
        step={step}
        defaultValue={defaultValue}
        onChange={(e) => {
          const target = e.target;
          target.parentElement?.style.setProperty(
            "--progress-track-factor",
            `${calcProgressTrackFactor(Number(target.value))}`
          );
          onChange?.(Number(target.value));
        }}
        className={inputStyle({ variant: orientation })}
        disabled={disabled}
        {...props}
      />
    </div>
  );
};
 
export { Slider };

API Reference

Extends the native input element with type="range".

PropDefaultTypeDescription

orientation

"horizontal"

"horizontal" | "vertical"

Orientation of the slider.

onChange

-

(value: number) => void

Callback function when value changes. Receives the numeric value as an argument.

The component uses CSS variables for customization:

  • --track-thickness - Thickness of the slider track
  • --thumb-size - Size of the slider thumb

Examples

Simple

Basic usage of the slider component.

Steps

Slider with defined step intervals.

Vertical

Vertical orientation of the slider.

Disabled

Disabled state of the slider.

Best Practices

  1. Usage Guidelines:

    • Always provide clear min and max values
    • Consider step intervals for precise control
    • Use appropriate slider length for the value range
    • Set a meaningful default value
  2. Accessibility:

    • Provide clear visual feedback
    • Include aria-labels for screen readers
    • Ensure keyboard navigation works (arrow keys)
  3. Interaction:

    • Provide immediate visual feedback
    • Consider adding value tooltips for precision
    • Handle both mouse and touch interactions