Sliding Reveal Card

The Sliding Reveal Card is an interactive UI component that lets users uncover hidden content by dragging a divider across the card. Built with smooth motion animations and responsive behavior, it creates an engaging reveal effect — ideal for comparisons, transformations, layered text, or dynamic content presentation in modern interfaces.

lucide-reactframer-motion
Slide to reveal the hidden content
✨ Hidden Content

Installation

Install the required dependencies:

npm install lucide-react framer-motion
components/ui/sliding-reveal-card.tsx
"use client";
  
  import { motion, useMotionValue, useTransform, useSpring } from "framer-motion";
  import { ChevronsLeftRight } from "lucide-react";
  import { useRef, useEffect, useState, ReactNode } from "react";
  
  type TextRevealCardProps = {
    leftContent?: ReactNode;
    rightContent?: ReactNode;
    height?: number | string;
  };
  
  export default function SlidingRevealCard({
    leftContent,
    rightContent,
    height = 220,
  }: TextRevealCardProps) {
    const containerRef = useRef<HTMLDivElement>(null);
    const rawX = useMotionValue(0);
    const x = useSpring(rawX, { stiffness: 250, damping: 30 }); // smooth drag
  
    const [width, setWidth] = useState(0);
  
    // Responsive width detection
    useEffect(() => {
      const updateWidth = () => {
        if (containerRef.current) {
          setWidth(containerRef.current.offsetWidth);
        }
      };
  
      updateWidth();
      window.addEventListener("resize", updateWidth);
      return () => window.removeEventListener("resize", updateWidth);
    }, []);
  
    const clipPath = useTransform(x, (val) => {
      const clamped = Math.max(0, Math.min(val, width));
      const percent = ((width - clamped) / width) * 100;
      return `inset(0 ${percent}% 0 0)`;
    });
  
    return (
      <div
        ref={containerRef}
        className="relative w-full max-w-2xl mx-auto my-24 rounded-2xl overflow-hidden 
                   border border-zinc-200 dark:border-zinc-700
                   shadow-xl bg-white dark:bg-zinc-900"
        style={{ height }}
      >
        {/* LEFT CONTENT (Revealed) */}
        <div
          className="absolute inset-0 p-8 flex items-center justify-center
                     text-2xl md:text-3xl font-medium
                     text-zinc-400 dark:text-zinc-600
                     bg-gradient-to-br from-white to-zinc-100
                     dark:from-zinc-900 dark:to-zinc-800"
        >
          {leftContent || "Slide to reveal the hidden content"}
        </div>
  
        {/* RIGHT CONTENT (Initially Visible) */}
        <motion.div
          style={{ clipPath }}
          className="absolute inset-0 p-8 flex items-center justify-center
                     text-2xl md:text-3xl font-semibold
                     text-black dark:text-white
                     bg-gradient-to-br from-zinc-100 to-white
                     dark:from-zinc-800 dark:to-zinc-900"
        >
          {rightContent || "✨ Hidden Content"}
        </motion.div>
  
        {/* DRAG HANDLE */}
        <motion.div
          drag="x"
          dragConstraints={containerRef}
          style={{ x }}
          className="absolute top-0 bottom-0 w-1 z-30 cursor-ew-resize"
        >
          {/* Gradient divider */}
          <div className="w-full h-full bg-gradient-to-b from-amber-400 via-amber-500 to-amber-600 shadow-xl" />
  
          <div
            className="absolute top-1/2 left-[0.5] -translate-y-1/2 -translate-x-1/2
                       w-8 h-8 rounded-full
                       bg-white dark:bg-zinc-800
                       border border-zinc-300 dark:border-zinc-600
                       flex items-center justify-center
                       shadow-lg"
          >
            <ChevronsLeftRight className="w-4 h-4 text-amber-600" />
          </div>
        </motion.div>
      </div>
    );
  }