Rejouice — loading0

Clip-reveal media

An image frame wipes open from the bottom while the picture inside scales down, then drifts on scroll.

Grade: ARuntimes: css · gsapStatus: live
Loading demo…
Clip-reveal media — scroll inside the frameOpen standalone ↗

Intent

A fade tells you an image loaded. A clip-reveal tells you it was placed. The frame opening and the image settling are two motions on one element, which is what makes a still photograph feel directed — and it lets the picture itself stay calm and premium while the motion does the drama.

Implementation

The shipped component. One ScrollTrigger drives both the wipe and the scale so they cannot desynchronise; the parallax is a second, scrubbed trigger.

Dependencies: gsap@^3.13, @gsap/react@^2, next@^16

next-tailwind-motion
"use client";

import { useRef } from "react";
import Image from "next/image";
import { gsap, useGSAP, EASE, DUR, prefersReduced } from "@/lib/gsap";

export function ClipRevealMedia({ src, alt, parallax = true }: {
  src: string; alt: string; parallax?: boolean;
}) {
  const frame = useRef<HTMLDivElement>(null);
  const inner = useRef<HTMLDivElement>(null);

  useGSAP(() => {
    if (prefersReduced() || !frame.current) return;

    gsap.set(frame.current, { clipPath: "inset(100% 0% 0% 0%)" });
    gsap.to(frame.current, {
      clipPath: "inset(0% 0% 0% 0%)",
      duration: DUR.curtain,
      ease: EASE.transition,
      scrollTrigger: { trigger: frame.current, start: "top 80%" },
    });

    // Same trigger, so the picture settles exactly as the frame finishes.
    gsap.from(inner.current, {
      scale: 1.2,
      duration: DUR.curtain,
      ease: EASE.transition,
      scrollTrigger: { trigger: frame.current, start: "top 80%" },
    });

    if (!parallax) return;
    gsap.to(inner.current, {
      yPercent: -12,
      ease: "none",
      scrollTrigger: {
        trigger: frame.current, start: "top bottom", end: "bottom top", scrub: true,
      },
    });
  }, { scope: frame });

  return (
    <div ref={frame} className="relative overflow-hidden aspect-[4/3]">
      {/* Taller than the frame so the drift never exposes an edge. */}
      <div ref={inner} className="absolute -top-[12%] h-[124%] w-full">
        <Image src={src} alt={alt} fill className="object-cover" />
      </div>
    </div>
  );
}

Use it when

  • Any hero or feature image. This is the signature media entrance.
  • Alternating two-column sections, where the wipe gives each image its own beat.

Avoid it when

  • Thumbnail grids — at small sizes the wipe reads as a glitch rather than a reveal.
  • Images already inside another animating container; two nested reveals cancel each other out.

Accessibility

  • Under reduced motion the frame renders fully open with the image centred — never leave a clip-path partially closed, or the picture is cropped for the people least able to notice why.
  • Decorative media takes `alt=""`; meaningful media takes a real description.
  • The reveal must not be the only cue that content exists below it.

Performance

  • Animate `clip-path` on the frame and `transform` on the image — both composite. Never animate width or height.
  • The inner wrapper must be taller than the frame by at least twice the drift distance, or the parallax exposes an edge at the extremes.
  • Give the image explicit dimensions so the frame reserves space before the reveal runs.

Knobs

parallax
Whether the image drifts after revealing. Default: true
travel
Drift distance as a percentage of frame height. Default: 12%
overscale
Inner height, so no edge is ever exposed. Default: 124%

Composition

Instruction for Claude Code

Build a clip-reveal media frame: an overflow-hidden container with a fixed aspect ratio whose clip-path animates from inset(100% 0 0 0) to inset(0) over the curtain duration with an ease-in-out-quart curve, triggered at top 80%. On the same trigger, scale the inner image from 1.2 to 1 so the frame and the picture resolve together. Size the inner wrapper to 124% height offset -12% so a scrubbed yPercent -12 parallax never exposes an edge. Under prefers-reduced-motion render the frame fully open and centred — never leave the clip-path partially closed. Animate clip-path and transform only.