Components
Interactive Slider
Interactive Slider
Smooth range sliders with animated thumbs, value indicators, and customizable styling.
Volume
6565
Brightness
8080
Speed
4545
Installation
1
Install the packages
npm i motion clsx tailwind-merge2
Add util file
lib/util.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3
Copy and paste the following code into your project
interactive-slider.tsx
"use client";
import { motion } from "motion/react";
import { useState, useRef, useEffect } from "react";
import { cn } from "@/lib/utils";
type InteractiveSliderProps = {
min?: number;
max?: number;
value?: number;
onChange?: (value: number) => void;
};
const InteractiveSlider = ({
min = 0,
max = 100,
value: initialValue = 50,
onChange = () => {},
}: InteractiveSliderProps) => {
const [value, setValue] = useState(initialValue);
const [isDragging, setIsDragging] = useState(false);
const sliderRef = useRef<HTMLDivElement>(null);
const handleMouseDown = (e: React.MouseEvent) => {
setIsDragging(true);
updateValue(e.clientX);
};
const handleMouseMove = (e: MouseEvent) => {
if (isDragging) {
updateValue(e.clientX);
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
const updateValue = (clientX: number) => {
if (sliderRef.current) {
const rect = sliderRef.current.getBoundingClientRect();
const percentage = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const newValue = Math.round(min + percentage * (max - min));
setValue(newValue);
onChange(newValue);
}
};
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}
}, [isDragging]);
const percentage = ((value - min) / (max - min)) * 100;
return (
<div className="w-full max-w-md mx-auto">
<div className="mb-6">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Value: {value}
</span>
<span className="text-xs text-neutral-500">
{min} - {max}
</span>
</div>
<div
ref={sliderRef}
className="relative h-6 bg-neutral-200 dark:bg-neutral-700 rounded-full cursor-pointer"
onMouseDown={handleMouseDown}
>
{/* Track */}
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full opacity-20" />
{/* Progress */}
<motion.div
className="absolute left-0 top-0 h-full bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${percentage}%` }}
transition={{ duration: 0.1 }}
/>
{/* Thumb */}
<motion.div
className="absolute top-1/2 -translate-y-1/2 w-6 h-6 bg-white border-2 border-blue-500 rounded-full shadow-lg cursor-grab active:cursor-grabbing"
style={{ left: `calc(${percentage}% - 12px)` }}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
animate={{
boxShadow: isDragging
? "0 0 20px rgba(59, 130, 246, 0.5)"
: "0 2px 8px rgba(0, 0, 0, 0.1)",
}}
transition={{ duration: 0.2 }}
/>
{/* Value indicator */}
<motion.div
className="absolute -top-8 left-1/2 -translate-x-1/2 bg-neutral-900 dark:bg-neutral-100 text-white dark:text-neutral-900 px-2 py-1 rounded text-xs font-medium"
style={{ left: `${percentage}%` }}
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
{value}
</motion.div>
</div>
</div>
</div>
);
};
export default InteractiveSlider;"use client";
import { motion } from "motion/react";
import { useState, useRef, useEffect } from "react";
import { cn } from "@/lib/utils";
type InteractiveSliderProps = {
min?: number;
max?: number;
value?: number;
onChange?: (value: number) => void;
};
const InteractiveSlider = ({
min = 0,
max = 100,
value: initialValue = 50,
onChange = () => {},
}: InteractiveSliderProps) => {
const [value, setValue] = useState(initialValue);
const [isDragging, setIsDragging] = useState(false);
const sliderRef = useRef<HTMLDivElement>(null);
const handleMouseDown = (e: React.MouseEvent) => {
setIsDragging(true);
updateValue(e.clientX);
};
const handleMouseMove = (e: MouseEvent) => {
if (isDragging) {
updateValue(e.clientX);
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
const updateValue = (clientX: number) => {
if (sliderRef.current) {
const rect = sliderRef.current.getBoundingClientRect();
const percentage = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const newValue = Math.round(min + percentage * (max - min));
setValue(newValue);
onChange(newValue);
}
};
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}
}, [isDragging]);
const percentage = ((value - min) / (max - min)) * 100;
return (
<div className="w-full max-w-md mx-auto">
<div className="mb-6">
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
Value: {value}
</span>
<span className="text-xs text-neutral-500">
{min} - {max}
</span>
</div>
<div
ref={sliderRef}
className="relative h-6 bg-neutral-200 dark:bg-neutral-700 rounded-full cursor-pointer"
onMouseDown={handleMouseDown}
>
{/* Track */}
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full opacity-20" />
{/* Progress */}
<motion.div
className="absolute left-0 top-0 h-full bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${percentage}%` }}
transition={{ duration: 0.1 }}
/>
{/* Thumb */}
<motion.div
className="absolute top-1/2 -translate-y-1/2 w-6 h-6 bg-white border-2 border-blue-500 rounded-full shadow-lg cursor-grab active:cursor-grabbing"
style={{ left: `calc(${percentage}% - 12px)` }}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
animate={{
boxShadow: isDragging
? "0 0 20px rgba(59, 130, 246, 0.5)"
: "0 2px 8px rgba(0, 0, 0, 0.1)",
}}
transition={{ duration: 0.2 }}
/>
{/* Value indicator */}
<motion.div
className="absolute -top-8 left-1/2 -translate-x-1/2 bg-neutral-900 dark:bg-neutral-100 text-white dark:text-neutral-900 px-2 py-1 rounded text-xs font-medium"
style={{ left: `${percentage}%` }}
initial={{ opacity: 0, y: -5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
{value}
</motion.div>
</div>
</div>
</div>
);
};
export default InteractiveSlider;4
Update the import paths to match your project setup
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| min | number | 0 | The minimum value of the slider. |
| max | number | 100 | The maximum value of the slider. |
| value | number | 50 | The current value of the slider. |