Elastic Slider

A sleek slider with smooth transitions, enhancing user experience with motion-focused animations.

Preview

Installation

Install dependencies

# Install Shadcn Slider component
npx shadcn@latest add slider
 
# Install Framer Motion
npm install framer-motion
 
# Install Lucide React for icons
npm install lucide-react

Create the component file

mkdir -p components/shsfui/sliders && touch components/shsfui/sliders/elastic-slider.tsx

Add the component code

Open the newly created file and paste the following code:

"use client";
 
import * as React from "react";
import {
  motion,
  type MotionValue,
  type MotionProps,
  useMotionValue,
  useMotionValueEvent,
  useTransform,
  animate,
  AnimatePresence,
} from "framer-motion";
import { Volume2, VolumeX } from "lucide-react";
import { Slider } from "@/components/ui/slider";
import { cn } from "@/utils/cn";
 
type VolumeSliderProps = {
  initialVolume?: number;
  min?: number;
  max?: number;
  step?: number;
  maxOverflow?: number;
  onVolumeChange?: (volume: number) => void;
  className?: string;
} & MotionProps;
 
type VolumeRegion = "left" | "middle" | "right";
 
const calculateDecay = (value: number, max: number): number => {
  if (max === 0) return 0;
 
  const entry = value / max;
  const sigmoid = 2 * (1 / (1 + Math.exp(-entry)) - 0.5);
 
  return sigmoid * max;
};
 
type VolumeIconProps = {
  icon: React.ReactNode;
  isActive: boolean;
  getOffset: () => number;
};
 
const VolumeIcon = ({ icon, isActive, getOffset }: VolumeIconProps) => {
  return (
    <motion.div
      animate={{
        scale: isActive ? [1, 1.4, 1] : 1,
        transition: { duration: 0.25 },
      }}
      style={{
        x: useTransform(getOffset),
      }}
    >
      {icon}
    </motion.div>
  );
};
 
type SliderContainerProps = {
  children: React.ReactNode;
  isInteracting: boolean;
  setIsInteracting: React.Dispatch<React.SetStateAction<boolean>>;
  clientX: MotionValue<number>;
  overflow: MotionValue<number>;
};
 
const SliderContainer = React.forwardRef<HTMLDivElement, SliderContainerProps>(
  ({ children, isInteracting, setIsInteracting, clientX, overflow }, ref) => {
    return (
      <div
        ref={ref}
        className="relative flex w-full max-w-[200px] grow cursor-grab touch-none select-none items-center py-4 active:cursor-grabbing"
        onPointerDown={() => setIsInteracting(true)}
        onPointerUp={() => setIsInteracting(false)}
        onPointerMove={(e) => {
          if (e.buttons > 0) {
            clientX.jump(e.clientX);
          }
        }}
        onLostPointerCapture={() => {
          animate(overflow, 0, { type: "spring", bounce: 0.5 });
          setIsInteracting(false);
        }}
      >
        {children}
      </div>
    );
  }
);
 
SliderContainer.displayName = "SliderContainer";
 
type VolumeTooltipProps = {
  isVisible: boolean;
  position: number;
  value: number;
};
 
const VolumeTooltip = ({ isVisible, position, value }: VolumeTooltipProps) => {
  return (
    <AnimatePresence>
      {isVisible && (
        <motion.div
          initial={{ opacity: 0, y: -20, scale: 0.8 }}
          animate={{ opacity: 1, y: -28, scale: 1 }}
          exit={{ opacity: 0, y: -10, scale: 0.8 }}
          style={{
            left: `${position}%`,
            x: "-50%",
          }}
          className="absolute top-0 bg-background/95 px-2 py-1 rounded-md text-xs font-medium shadow-md border border-border/30 pointer-events-none"
        >
          {value}%
          <div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 w-2 h-2 bg-background/95 rotate-45 border-r border-b border-border/30"></div>
        </motion.div>
      )}
    </AnimatePresence>
  );
};
 
const VolumeSlider = React.forwardRef<HTMLDivElement, VolumeSliderProps>(
  (props, ref) => {
    const {
      className,
      initialVolume = 50,
      min = 0,
      max = 100,
      step = 0.01,
      maxOverflow = 50,
      onVolumeChange,
      ...restProps
    } = props;
 
    const [volume, setVolume] = React.useState(initialVolume);
    const [region, setRegion] = React.useState<VolumeRegion>("middle");
    const [isInteracting, setIsInteracting] = React.useState(false);
    const [tooltipPosition, setTooltipPosition] = React.useState(initialVolume);
 
    const sliderRef = React.useRef<HTMLDivElement>(null);
 
    const clientX = useMotionValue(0);
    const overflow = useMotionValue(0);
    const scale = useMotionValue(1);
 
    useMotionValueEvent(clientX, "change", (latest) => {
      if (!sliderRef.current) return;
 
      const { left, right } = sliderRef.current.getBoundingClientRect();
      let overflowValue = 0;
 
      if (latest < left) {
        setRegion("left");
        overflowValue = left - latest;
      } else if (latest > right) {
        setRegion("right");
        overflowValue = latest - right;
      } else {
        setRegion("middle");
 
        const sliderWidth = right - left;
        const relativeX = latest - left;
        const positionPercentage = (relativeX / sliderWidth) * 100;
        setTooltipPosition(Math.max(0, Math.min(100, positionPercentage)));
      }
 
      overflow.jump(calculateDecay(overflowValue, maxOverflow));
    });
 
    const handleVolumeChange = (values: number[]) => {
      const newVolume = Math.floor(values[0]);
      setVolume(newVolume);
 
      if (onVolumeChange) {
        onVolumeChange(newVolume);
      }
    };
 
    React.useEffect(() => {
      if (!isInteracting || region === "middle") {
        setTooltipPosition(volume);
      }
    }, [volume, isInteracting, region]);
 
    const getSliderScaleX = (): number => {
      if (!sliderRef.current) return 1;
 
      const { width } = sliderRef.current.getBoundingClientRect();
      return 1 + overflow.get() / width;
    };
 
    const getTransformOrigin = (): string => {
      if (!sliderRef.current) return "center";
 
      const { left, width } = sliderRef.current.getBoundingClientRect();
      return clientX.get() < left + width / 2 ? "right" : "left";
    };
 
    const getIconOffset = (side: "left" | "right"): number => {
      return side === region ? overflow.get() / scale.get() : 0;
    };
 
    const scaleOpacity = useTransform(scale, [1, 1.2], [0.7, 1]);
    const scaleY = useTransform(overflow, [0, maxOverflow], [1, 0.8]);
    const sliderHeight = useTransform(scale, [1, 1.2], [6, 12]);
    const sliderMargin = useTransform(scale, [1, 1.2], [0, -3]);
    const transformOrigin = useTransform(getTransformOrigin);
 
    const handleTouchStart = () => {
      setIsInteracting(true);
      animate(scale, 1.2);
    };
 
    const handleTouchEnd = () => {
      setIsInteracting(false);
      animate(scale, 1);
    };
 
    return (
      <motion.div
        ref={ref}
        onTouchStart={handleTouchStart}
        onTouchEnd={handleTouchEnd}
        style={{
          scale,
          opacity: scaleOpacity,
        }}
        className={cn(
          "flex w-full touch-none select-none items-center justify-center gap-3",
          className
        )}
        {...restProps}
      >
        <VolumeIcon
          icon={<VolumeX className="size-5 opacity-70" />}
          isActive={region === "left"}
          getOffset={() => (region === "left" ? -getIconOffset("left") : 0)}
        />
 
        <SliderContainer
          ref={sliderRef}
          isInteracting={isInteracting}
          setIsInteracting={setIsInteracting}
          clientX={clientX}
          overflow={overflow}
        >
          <VolumeTooltip
            isVisible={isInteracting || scale.get() > 1}
            position={tooltipPosition}
            value={volume}
          />
 
          <motion.div
            style={{
              scaleX: useTransform(getSliderScaleX),
              scaleY,
              transformOrigin,
              height: sliderHeight,
              marginTop: sliderMargin,
              marginBottom: sliderMargin,
            }}
            className="w-full"
          >
            <Slider
              value={[volume]}
              onValueChange={handleVolumeChange}
              min={min}
              max={max}
              step={step}
              className="relative"
            />
          </motion.div>
        </SliderContainer>
 
        <VolumeIcon
          icon={<Volume2 className="size-5 opacity-70" />}
          isActive={region === "right"}
          getOffset={() => (region === "right" ? getIconOffset("right") : 0)}
        />
      </motion.div>
    );
  }
);
 
VolumeSlider.displayName = "VolumeSlider";
 
export VolumeSlider;