Velocity UI
Loading…
Menu

Before After Comparison Slider

Draggable before/after slider with shimmer handle for visual comparisons. Light/Dark compatible.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add before-after-comparison-slider

Source Code

before-after-comparison-slider.tsx
'use client'

import React, { useEffect, useRef, useState } from 'react'
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion'

export default function PremiumImageSlider() {
  const containerRef = useRef<HTMLDivElement>(null)
  const [dims, setDims] = useState({ width: 0, height: 0 })
  
  const x = useMotionValue(0)
  const springX = useSpring(x, { stiffness: 450, damping: 40 })

  // Calculate the clipping mask for the colored image
  const clipPath = useTransform(springX, (val) => `inset(0 0 0 ${val}px)`)

  useEffect(() => {
    if (containerRef.current) {
      const { clientWidth, clientHeight } = containerRef.current
      setDims({ width: clientWidth, height: clientHeight })
      x.set(clientWidth / 2)
    }
  }, [x])

  const handleDrag = (_: any, info: any) => {
    const rect = containerRef.current?.getBoundingClientRect()
    if (rect) {
      const newX = Math.max(0, Math.min(info.point.x - rect.left, dims.width))
      x.set(newX)
    }
  }

  // Placeholder images - Replace with your own high-res URLs
  const imageUrl = "https://images.unsplash.com/photo-1550684848-fac1c5b4e853?q=80&w=2070&auto=format&fit=crop"

  return (
    <div className="w-full grid place-items-center py-16">
      {/* OUTER GLASS BORDER */}
      <div className="p-[1px] rounded-[2.2rem] bg-gradient-to-b from-black/20 to-transparent shadow-2xl">
        
        {/* INNER THIN BORDER */}
        <div className="p-1 rounded-[2.1rem] bg-white border border-black/[0.03] shadow-inner">
          
          <div
            ref={containerRef}
            className="relative w-[340px] h-[450px] sm:w-[550px] sm:h-[400px] rounded-[1.8rem] overflow-hidden bg-neutral-100 select-none shadow-2xl"
          >
            {/* LAYER 1: BEFORE (Black & White) */}
            <div className="absolute inset-0 z-0">
              <img 
                src={imageUrl} 
                alt="Before" 
                className="w-full h-full object-cover grayscale brightness-90"
              />
              <div className="absolute inset-0 bg-black/10" />
            </div>

            {/* LAYER 2: AFTER (Color + Masked) */}
            <motion.div
              className="absolute inset-0 z-10"
              style={{ clipPath }}
            >
              <img 
                src={imageUrl} 
                alt="After" 
                className="w-full h-full object-cover"
                style={{ width: dims.width }} 
              />
            </motion.div>

            {/* FLOATING GLOW DOTS (SVG) */}
            <div className="absolute inset-0 z-20 pointer-events-none">
              <svg width="100%" height="100%">
                <defs>
                  <filter id="softGlow">
                    <feGaussianBlur stdDeviation="2" result="blur" />
                    <feComposite in="SourceGraphic" in2="blur" operator="over" />
                  </filter>
                </defs>
                {[...Array(8)].map((_, i) => (
                  <motion.circle
                    key={i}
                    r="1.5"
                    fill="white"
                    filter="url(#softGlow)"
                    initial={{ x: Math.random() * 550, y: Math.random() * 400 }}
                    animate={{ opacity: [0.1, 0.4, 0.1], scale: [1, 1.2, 1] }}
                    transition={{ duration: 3 + i, repeat: Infinity }}
                  />
                ))}
              </svg>
            </div>

            {/* THE PREMIUM HANDLE */}
            <motion.div
              drag="x"
              dragConstraints={containerRef}
              dragElastic={0}
              onDrag={handleDrag}
              style={{ x: springX }}
              className="absolute inset-y-0 -ml-[1.5px] w-[3px] z-30 cursor-ew-resize bg-white shadow-[0_0_15px_rgba(0,0,0,0.3)]"
            >
              {/* Central Trigger */}
              <div className="absolute top-1/2 -left-5 -translate-y-1/2">
                <motion.div 
                  whileHover={{ scale: 1.05 }}
                  whileTap={{ scale: 0.95 }}
                  className="w-10 h-10 rounded-2xl bg-white/90 backdrop-blur-md border border-black/5 shadow-xl flex items-center justify-center"
                >
                  <div className="flex gap-1">
                    <div className="w-[1.5px] h-4 bg-black/20 rounded-full" />
                    <div className="w-[1.5px] h-4 bg-black/60 rounded-full" />
                    <div className="w-[1.5px] h-4 bg-black/20 rounded-full" />
                  </div>
                </motion.div>
              </div>
            </motion.div>

            {/* MINI LABELS */}
            <div className="absolute top-5 left-5 z-20">
              <span className="text-[9px] uppercase tracking-[0.3em] font-bold text-white bg-black/20 backdrop-blur-md px-2 py-1 rounded-md border border-white/10">
                Legacy
              </span>
            </div>
            <div className="absolute top-5 right-5 z-20">
              <span className="text-[9px] uppercase tracking-[0.3em] font-bold text-black bg-white/40 backdrop-blur-md px-2 py-1 rounded-md border border-black/5">
                Enhanced
              </span>
            </div>

          </div>
        </div>
      </div>
    </div>
  )
}

Props

Component property reference.

NameTypeDefaultDescription
beforeSrcstring-Image source for the “before” side.
afterSrcstring-Image source for the “after” side.
classNamestring-Additional CSS classes.
Context Worth Keeping In Orbit

Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.

I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.

I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.