Build websites 10x faster with HextaUI Blocks — Learn more
UI/UI

Slider

A customizable range input component for selecting numeric values with multiple variants and sizes.

Preview

25

Adjust the volume level

import { Slider } from "@/src/components/ui/Slider";

export function SliderWithLabel() {
  const [value, setValue] = React.useState([25]);

  return (
    <Slider
      value={value}
      onValueChange={setValue}
      label="Volume"
      description="Adjust the volume level"
      showValue
      max={100}
      step={1}
    />
  );
}

Installation

Install the required dependencies:

npm install @radix-ui/react-slider class-variance-authority
pnpm add @radix-ui/react-slider class-variance-authority
yarn add @radix-ui/react-slider class-variance-authority
bun add @radix-ui/react-slider class-variance-authority

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 { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const sliderVariants = cva(
  "relative flex w-full touch-none select-none items-center",
  {
    variants: {
      variant: {
        default: "",
        destructive: "",
        ghost: "",
      },
      size: {
        sm: "h-3",
        default: "h-4",
        lg: "h-5",
        xl: "h-6",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

const sliderTrackVariants = cva(
  "relative w-full grow overflow-hidden rounded-full",
  {
    variants: {
      variant: {
        default: "bg-[hsl(var(--hu-muted))]",
        destructive: "bg-[hsl(var(--hu-destructive))]/20",
        ghost: "bg-[hsl(var(--hu-accent))]",
      },
      size: {
        sm: "h-1.5",
        default: "h-2",
        lg: "h-2.5",
        xl: "h-3",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

const sliderRangeVariants = cva(
  "absolute h-full rounded-full transition-all",
  {
    variants: {
      variant: {
        default: "bg-[hsl(var(--hu-primary))]",
        destructive: "bg-[hsl(var(--hu-destructive))]",
        ghost: "bg-[hsl(var(--hu-foreground))]",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
);

const sliderThumbVariants = cva(
  "block rounded-full border-2 bg-[hsl(var(--hu-background))] transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--hu-ring))] focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:shadow-md",
  {
    variants: {
      variant: {
        default: "border-[hsl(var(--hu-primary))] hover:border-[hsl(var(--hu-primary))]/80",
        destructive: "border-[hsl(var(--hu-destructive))] hover:border-[hsl(var(--hu-destructive))]/80",
        ghost: "border-[hsl(var(--hu-foreground))] hover:border-[hsl(var(--hu-foreground))]/80",
      },
      size: {
        sm: "h-3 w-3",
        default: "h-4 w-4",
        lg: "h-5 w-5",
        xl: "h-6 w-6",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

export interface SliderProps
  extends Omit<React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>, "size">,
    VariantProps<typeof sliderVariants> {
  label?: string;
  description?: string;
  error?: boolean | string;
  showValue?: boolean;
  showMinMax?: boolean;
  formatValue?: (value: number) => string;
  onValueChange?: (value: number[]) => void;
}

const Slider = React.forwardRef<
  React.ElementRef<typeof SliderPrimitive.Root>,
  SliderProps
>(
  (
    {
      className,
      variant,
      size,
      label,
      description,
      error,
      showValue = false,
      showMinMax = false,
      formatValue = (value) => value.toString(),
      min = 0,
      max = 100,
      step = 1,
      value,
      defaultValue,
      onValueChange,
      disabled,
      orientation = "horizontal",
      ...props
    },
    ref
  ) => {
    const [internalValue, setInternalValue] = React.useState<number[]>(
      defaultValue || value || [min]
    );

    const currentValue = value || internalValue;
    const effectiveVariant = error ? "destructive" : variant;

    const handleValueChange = React.useCallback(
      (newValue: number[]) => {
        if (!value) {
          setInternalValue(newValue);
        }
        onValueChange?.(newValue);
      },
      [value, onValueChange]
    );

    const sliderId = React.useId();
    const descriptionId = React.useId();

    const sliderElement = (
      <SliderPrimitive.Root
        ref={ref}
        id={sliderId}
        className={cn(sliderVariants({ variant: effectiveVariant, size }), className)}
        value={currentValue}
        onValueChange={handleValueChange}
        min={min}
        max={max}
        step={step}
        disabled={disabled}
        orientation={orientation}
        aria-describedby={description ? descriptionId : undefined}
        {...props}
      >
        <SliderPrimitive.Track
          className={cn(sliderTrackVariants({ variant: effectiveVariant, size }))}
        >
          <SliderPrimitive.Range
            className={cn(sliderRangeVariants({ variant: effectiveVariant }))}
          />
        </SliderPrimitive.Track>
        {currentValue.map((_, index) => (
          <SliderPrimitive.Thumb
            key={index}
            className={cn(sliderThumbVariants({ variant: effectiveVariant, size }))}
          />
        ))}
      </SliderPrimitive.Root>
    );

    if (label || description || showValue || showMinMax || error) {
      return (
        <div className="space-y-2">
          {/* Header with label and value */}
          {(label || showValue) && (
            <div className="flex items-center justify-between">
              {label && (
                <label
                  htmlFor={sliderId}
                  className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
                >
                  {label}
                </label>
              )}
              {showValue && (
                <span className="text-sm text-[hsl(var(--hu-muted-foreground))]">
                  {currentValue.length === 1
                    ? formatValue(currentValue[0])
                    : currentValue.map(formatValue).join(" - ")}
                </span>
              )}
            </div>
          )}

          {/* Slider */}
          <div className="space-y-2">
            {sliderElement}

            {/* Min/Max labels */}
            {showMinMax && (
              <div className="flex justify-between text-xs text-[hsl(var(--hu-muted-foreground))]">
                <span>{formatValue(min)}</span>
                <span>{formatValue(max)}</span>
              </div>
            )}
          </div>

          {/* Description and error */}
          {(description || error) && (
            <div className="space-y-1">
              {description && (
                <p
                  id={descriptionId}
                  className="text-sm text-[hsl(var(--hu-muted-foreground))]"
                >
                  {description}
                </p>
              )}
              {error && typeof error === "string" && (
                <p className="text-sm text-[hsl(var(--hu-destructive))]">
                  {error}
                </p>
              )}
            </div>
          )}
        </div>
      );
    }

    return sliderElement;
  }
);

Slider.displayName = SliderPrimitive.Root.displayName;

export { Slider };

Update the import paths to match your project structure.

Usage

import { Slider } from "@/components/ui/Slider";
<Slider defaultValue={[50]} max={100} step={1} />

Examples

Basic Slider

export function BasicSlider() {
  return (
    <div className="w-full max-w-md space-y-4">
      <Slider defaultValue={[50]} max={100} step={1} />
    </div>
  );
}

Variants

export function SliderVariants() {
  return (
    <div className="w-full max-w-md space-y-6">
      <div>
        <label className="text-sm font-medium mb-2 block">Default</label>
        <Slider defaultValue={[30]} max={100} step={1} />
      </div>

      <div>
        <label className="text-sm font-medium mb-2 block">Destructive</label>
        <Slider defaultValue={[70]} max={100} step={1} variant="destructive" />
      </div>

      <div>
        <label className="text-sm font-medium mb-2 block">Ghost</label>
        <Slider defaultValue={[50]} max={100} step={1} variant="ghost" />
      </div>
    </div>
  );
}

Sizes

export function SliderSizes() {
  return (
    <div className="w-full max-w-md space-y-6">
      <div>
        <label className="text-sm font-medium mb-2 block">Small</label>
        <Slider defaultValue={[25]} max={100} step={1} size="sm" />
      </div>

      <div>
        <label className="text-sm font-medium mb-2 block">Default</label>
        <Slider defaultValue={[50]} max={100} step={1} size="default" />
      </div>

      <div>
        <label className="text-sm font-medium mb-2 block">Large</label>
        <Slider defaultValue={[75]} max={100} step={1} size="lg" />
      </div>

      <div>
        <label className="text-sm font-medium mb-2 block">Extra Large</label>
        <Slider defaultValue={[90]} max={100} step={1} size="xl" />
      </div>
    </div>
  );
}

With Min/Max Labels

50°C
0°C100°C

Set the desired temperature

export function SliderWithMinMax() {
  const [value, setValue] = React.useState([50]);

  return (
    <div className="w-full max-w-md space-y-4">
      <Slider
        value={value}
        onValueChange={setValue}
        label="Temperature"
        description="Set the desired temperature"
        showValue
        showMinMax
        min={0}
        max={100}
        step={5}
        formatValue={(val) => `${val}°C`}
      />
    </div>
  );
}

Range Slider

$20 - $80
$0$1000

Select your budget range

export function RangeSlider() {
  const [value, setValue] = React.useState([20, 80]);

  return (
    <div className="w-full max-w-md space-y-4">
      <Slider
        value={value}
        onValueChange={setValue}
        label="Price Range"
        description="Select your budget range"
        showValue
        showMinMax
        min={0}
        max={1000}
        step={10}
        formatValue={(val) => `$${val}`}
      />
    </div>
  );
}

Stepped Slider

Good

Rate your experience

export function SteppedSlider() {
  const [value, setValue] = React.useState([3]);

  const formatRating = (val: number) => {
    const ratings = ["Poor", "Fair", "Good", "Great", "Excellent"];
    return ratings[val - 1] || "Unknown";
  };

  return (
    <div className="w-full max-w-md space-y-4">
      <Slider
        value={value}
        onValueChange={setValue}
        label="Rating"
        description="Rate your experience"
        showValue
        min={1}
        max={5}
        step={1}
        formatValue={formatRating}
      />
    </div>
  );
}

Disabled State

60

This slider is disabled

export function DisabledSlider() {
  return (
    <div className="w-full max-w-md space-y-4">
      <Slider
        defaultValue={[60]}
        max={100}
        step={1}
        disabled
        label="Disabled Slider"
        description="This slider is disabled"
        showValue
      />
    </div>
  );
}

With Error State

95%
0%100%

Current CPU usage percentage

CPU usage is critically high!

export function SliderWithError() {
  const [value, setValue] = React.useState([95]);

  return (
    <div className="w-full max-w-md space-y-4">
      <Slider
        value={value}
        onValueChange={setValue}
        label="CPU Usage"
        description="Current CPU usage percentage"
        showValue
        showMinMax
        max={100}
        step={1}
        error={value[0] > 90 ? "CPU usage is critically high!" : false}
        formatValue={(val) => `${val}%`}
      />
    </div>
  );
}

Form Integration

Display Settings

75%
0%100%

Adjust system volume

50%

Screen brightness level

60%

Screen contrast level

$100 - $500
$0$1000

Budget range for purchases

export function FormExample() {
  const [settings, setSettings] = React.useState({
    volume: [75],
    brightness: [50],
    contrast: [60],
    priceRange: [100, 500],
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log("Settings:", settings);
    alert("Settings saved!");
  };

  return (
    <form onSubmit={handleSubmit} className="w-full max-w-md space-y-6">
      <h3 className="text-lg font-semibold">Display Settings</h3>

      <Slider
        value={settings.volume}
        onValueChange={(value) => setSettings(s => ({ ...s, volume: value }))}
        label="Volume"
        description="Adjust system volume"
        showValue
        showMinMax
        max={100}
        step={1}
        formatValue={(val) => `${val}%`}
      />

      <Slider
        value={settings.brightness}
        onValueChange={(value) => setSettings(s => ({ ...s, brightness: value }))}
        label="Brightness"
        description="Screen brightness level"
        showValue
        max={100}
        step={5}
        formatValue={(val) => `${val}%`}
      />

      <Slider
        value={settings.contrast}
        onValueChange={(value) => setSettings(s => ({ ...s, contrast: value }))}
        label="Contrast"
        description="Screen contrast level"
        showValue
        max={100}
        step={5}
        formatValue={(val) => `${val}%`}
      />

      <Slider
        value={settings.priceRange}
        onValueChange={(value) => setSettings(s => ({ ...s, priceRange: value }))}
        label="Price Range"
        description="Budget range for purchases"
        showValue
        showMinMax
        min={0}
        max={1000}
        step={25}
        formatValue={(val) => `$${val}`}
      />

      <Button
        type="submit"
        className="w-full px-4 py-2 bg-[hsl(var(--hu-primary))] text-[hsl(var(--hu-primary-foreground))] rounded-[var(--radius)] hover:bg-[hsl(var(--hu-primary))]/90 transition-colors"
      >
        Save Settings
      </Button>
    </form>
  );
}

Props

PropTypeDefault
orientation?
"horizontal" | "vertical"
"horizontal"
disabled?
boolean
false
step?
number
1
max?
number
100
min?
number
0
onValueChange?
(value: number[]) => void
undefined
defaultValue?
number[]
[min]
value?
number[]
undefined
formatValue?
(value: number) => string
(value) => value.toString()
showMinMax?
boolean
false
showValue?
boolean
false
error?
boolean | string
false
description?
string
undefined
label?
string
undefined
size?
"sm" | "default" | "lg" | "xl"
"default"
variant?
"default" | "destructive" | "ghost"
"default"
Edit on GitHub

Last updated on