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
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
Prop | Type | Default |
---|---|---|
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