Edu Calvo

SVG Clip Mask

Component 15

About

A container that reveals its children through a custom SVG shape mask (blob, arch, diamond, wave, or a custom path) that can morph on a loop, on scroll, or on hover.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds SVG Clip Mask to your project and fits it to your theme.

Code

How SVG Clip Mask is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import SvgClipMask from "@/components/ui/svg-clip-mask";
import { ArrowDown } from "lucide-react";
import { useRef } from "react";

const MORPH_PATHS = [
  "M0.5,0.02 C0.75,0.02 0.98,0.25 0.98,0.5 C0.98,0.75 0.75,0.98 0.5,0.98 C0.25,0.98 0.02,0.75 0.02,0.5 C0.02,0.25 0.25,0.02 0.5,0.02 Z",
  "M0.5,0.1 C0.85,0.05 0.95,0.35 0.9,0.55 C0.95,0.85 0.65,0.95 0.5,0.9 C0.15,0.98 0.05,0.65 0.1,0.45 C0.05,0.15 0.35,0.05 0.5,0.1 Z",
];

// `animate="scroll"` steps between the paths it is given, so it needs at least
// two — with a single path it has nothing to step to and silently does nothing,
// which is what the wave below used to do. Every path keeps the same command
// structure (M, C, L, L, Z) so the browser interpolates the numbers instead of
// snapping between shapes.
const WAVE_PATHS = [
  "M0,0.42 C0.25,0.00 0.75,0.72 1,0.36 L1,1 L0,1 Z",
  "M0,0.10 C0.25,0.70 0.75,0.04 1,0.58 L1,1 L0,1 Z",
  "M0,0.55 C0.25,0.08 0.75,0.66 1,0.06 L1,1 L0,1 Z",
  "M0,0.06 C0.25,0.62 0.75,0.00 1,0.48 L1,1 L0,1 Z",
  "M0,0.48 C0.25,0.02 0.75,0.60 1,0.20 L1,1 L0,1 Z",
];

export default function SvgClipMaskDemo() {
  const scrollRef = useRef<HTMLDivElement>(null);

  return (
    <div className="relative w-full overflow-hidden rounded-2xl border border-foreground/12 bg-background">
      <div
        // `relative` is load-bearing: Motion walks `offsetParent` from the
        // target up to the scroll container, so the container has to be one.
        // Without it the offsets are shifted by the panel's distance down the
        // document and the crest no longer tracks the scroll you can see.
        className="relative h-[500px] overflow-y-auto overscroll-contain"
        ref={scrollRef}
      >
        <div className="flex flex-col items-center gap-4 px-6 pt-8 pb-6">
          <p className="max-w-sm text-center text-muted-foreground text-sm leading-relaxed">
            The blob morphs on a loop of its own, with no scrolling involved.
          </p>
          <SvgClipMask animate="morph" morphPaths={MORPH_PATHS} shape="blob">
            <img
              alt="Amber and violet abstract color fields"
              className="size-52 object-cover"
              src="https://cdn.21st.dev/assets/localized/e1575b5bdd49c45a88da1ca0769f0870cf936416615f70944c7c2337b89e6d65.jpg"
            />
          </SvgClipMask>
        </div>

        <div className="flex h-[280px] items-end justify-center px-6 pb-4">
          <p className="max-w-sm text-center text-muted-foreground text-sm leading-relaxed">
            The wave below is different — its mask is bound to this
            panel&rsquo;s scroll position, so the crest moves as you scroll.
          </p>
        </div>

        <div className="px-6">
          <SvgClipMask
            animate="scroll"
            className="w-full"
            container={scrollRef}
            morphPaths={WAVE_PATHS}
            shape="wave"
          >
            <img
              alt="Cobalt blue and pink abstract color fields"
              className="h-48 w-full object-cover"
              src="https://cdn.21st.dev/assets/localized/7c29ce50766406289dd48f94938f38d14dcdcc2f2a8b83ae4307fb96394f91f5.jpg"
            />
          </SvgClipMask>
        </div>

        <div className="flex h-[300px] items-start justify-center px-6 pt-10">
          <p className="max-w-sm text-center text-muted-foreground/70 text-sm leading-relaxed">
            Scroll back up and the crest retraces its path — the shape is bound
            to scroll position, not to a one-shot trigger.
          </p>
        </div>
      </div>

      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-x-0 bottom-0 h-16 bg-gradient-to-t from-background to-transparent"
      />
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-x-0 bottom-3 flex justify-center"
      >
        <span className="inline-flex items-center gap-1.5 rounded-full border border-foreground/12 bg-background/90 px-3 py-1 font-medium text-muted-foreground text-xs shadow-black/5 shadow-sm backdrop-blur">
          <ArrowDown className="size-3.5" />
          Scroll inside the panel
        </span>
      </div>
    </div>
  );
}

Dependencies

More by Edu Calvo

View all
  • Scrollable Card Stack

    Scrollable Card Stack

    235
  • AI Task List

    AI Task List

    160
  • Siri Orb

    Siri Orb

    160
  • Cursor Follow

    Cursor Follow

    154
  • Pixel Flow Field

    Pixel Flow Field

    142
  • Agent Avatar

    Agent Avatar

    130
  • Gravity Stars

    Gravity Stars

    112
  • Coverflow Carousel

    Coverflow Carousel

    95