Web InnoventixFreeCode

Caption Mask Effect

Original · free

A caption sits in a knockout bar over the image.

byWeb InnoventixReact + Tailwind
captionmaskimageeffect
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/imgfx-caption-mask.json
imgfx-caption-mask.tsx
"use client";

import { useRef } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";

const PHOTO = "/img/gallery/g14.webp";

export default function ImgfxCaptionMask() {
  const ref = useRef<HTMLDivElement>(null);
  const inView = useInView(ref, { once: true, margin: "-15% 0px" });
  const reduce = useReducedMotion();

  const rise = {
    initial: reduce ? { opacity: 1 } : { opacity: 0, y: 26 },
    animate: inView ? { opacity: 1, y: 0 } : undefined,
  };

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 px-6 py-20 text-slate-900 sm:py-28 dark:bg-neutral-950 dark:text-slate-100">
      <style>{`
        @keyframes imcm-drift {
          0%, 100% { background-position: 50% 38%; }
          50%      { background-position: 50% 62%; }
        }
        @keyframes imcm-sweep {
          0%   { transform: translateX(-120%); opacity: 0; }
          22%  { opacity: 1; }
          55%  { opacity: 0; }
          100% { transform: translateX(120%); opacity: 0; }
        }
        .imcm-knock {
          -webkit-background-clip: text;
          background-clip: text;
          color: transparent;
          background-size: 130% auto;
          background-position: 50% 50%;
          animation: imcm-drift 11s ease-in-out infinite;
        }
        .imcm-sweep {
          animation: imcm-sweep 6s ease-in-out infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .imcm-knock { animation: none; background-position: 50% 50%; }
          .imcm-sweep { animation: none; opacity: 0; }
        }
      `}</style>

      <div className="mx-auto max-w-3xl">
        <motion.div {...rise} transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}>
          <p className="mb-3 text-[0.7rem] font-semibold uppercase tracking-[0.28em] text-indigo-600 dark:text-indigo-400">
            Image Effect · Caption Mask
          </p>
          <h2 className="text-3xl font-black tracking-tight sm:text-4xl">
            The caption is cut from the frame
          </h2>
          <p className="mt-3 max-w-xl text-sm leading-relaxed text-slate-500 dark:text-slate-400">
            A solid bar sits over the photo, and the headline is knocked straight
            out of it — so the image itself becomes the ink of every letter.
          </p>
        </motion.div>

        <motion.figure
          {...rise}
          transition={{ duration: 0.7, ease: [0.22, 1, 0.36, 1], delay: reduce ? 0 : 0.1 }}
          className="group relative mt-9 overflow-hidden rounded-2xl shadow-xl shadow-slate-900/10 ring-1 ring-slate-900/10 dark:shadow-black/40 dark:ring-white/10"
        >
          <div ref={ref} className="relative aspect-[4/5] w-full sm:aspect-[16/10]">
            {/* eslint-disable-next-line @next/next/no-img-element */}
            <img
              src={PHOTO}
              alt="Fog rolling over a layered mountain ridge at first light"
              loading="lazy"
              draggable={false}
              className="absolute inset-0 h-full w-full object-cover transition-transform duration-[1200ms] ease-out group-hover:scale-[1.04]"
            />

            {/* readability wash under the caption bar */}
            <div className="pointer-events-none absolute inset-x-0 bottom-0 h-2/3 bg-gradient-to-t from-black/45 via-black/10 to-transparent" />

            {/* the masked caption bar */}
            <figcaption className="absolute inset-x-0 bottom-0">
              <div className="relative overflow-hidden border-t border-white/40 bg-white/85 px-6 py-5 backdrop-blur-md transition-[padding] duration-500 ease-out group-hover:pb-7 sm:px-8 dark:border-white/10 dark:bg-slate-950/70">
                {/* moving highlight sweep across the bar */}
                <div
                  aria-hidden
                  className="imcm-sweep pointer-events-none absolute inset-y-0 left-0 w-1/3 bg-gradient-to-r from-transparent via-white/60 to-transparent mix-blend-overlay dark:via-white/25"
                />

                <p className="mb-1.5 text-[0.62rem] font-semibold uppercase tracking-[0.34em] text-slate-500 dark:text-slate-400">
                  Field Notes · No. 07
                </p>

                {/* knockout headline — image shows through the letters */}
                <span
                  className="imcm-knock block text-4xl font-black leading-[0.92] tracking-tight sm:text-5xl"
                  style={{ backgroundImage: `url(${PHOTO})` }}
                >
                  Coastal Dawn
                </span>

                <p className="mt-2 text-xs text-slate-600 dark:text-slate-300">
                  Shot on the north ridge, 05:41 — the exact minute the haze broke.
                </p>
              </div>
            </figcaption>
          </div>
        </motion.figure>

        <p className="mt-5 text-center text-xs text-slate-400 dark:text-slate-500">
          Hover the frame — the photo drifts inside the type while the bar holds its shape.
        </p>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →