Hero with Word Reveal Headline and Floating Cards

A dark, centred hero where the headline rises into place one word at a time, four product cards float around the text and a call to action sits inside a rotating gradient border with a soft pulsing glow behind it. The cards react to the pointer with a depth effect and keep drifting on their own when nobody touches the page. On phones and tablets the cards move into a small cluster under the buttons, so nothing covers the text.

Code

import { useEffect, useRef, useState } from "react";
import { motion, useMotionValue, useReducedMotion, useSpring, useTransform, type MotionValue } from "framer-motion";

const headline = "Send the invoice. Get paid by dinner.";
/** Words from this index on are shown in the accent colour. */
const accentFrom = 3;

const cards = [
  { kind: "invoice", pos: "left-0 top-0 xl:left-[3%] xl:top-[13%]", rotate: -7, depth: 26, drift: 7.5 },
  { kind: "chart", pos: "right-0 top-4 xl:right-[3%] xl:top-[10%]", rotate: 6, depth: 34, drift: 9 },
  { kind: "payout", pos: "left-[22%] top-36 xl:left-[7%] xl:top-[60%]", rotate: 4, depth: 46, drift: 8.2 },
  { kind: "paid", pos: "hidden xl:block xl:right-[6%] xl:top-[62%]", rotate: -4, depth: 40, drift: 6.8 },
] as const;

const bars = [38, 52, 44, 66, 58, 82, 100];

function CardBody({ kind }: { kind: (typeof cards)[number]["kind"] }) {
  if (kind === "invoice")
    return (
      <>
        <div className="flex items-center justify-between text-xs text-stone-400">
          <span>Invoice 2041</span>
          <span className="rounded-full bg-emerald-400/15 px-2 py-0.5 font-medium text-emerald-300">Paid</span>
        </div>
        <p className="mt-3 text-2xl font-semibold tracking-tight text-white">$2,400.00</p>
        <p className="mt-1 text-xs text-stone-400">Fernhill Studio, paid in 3 hours</p>
      </>
    );
  if (kind === "chart")
    return (
      <>
        <p className="text-xs text-stone-400">Collected this week</p>
        <p className="mt-1 text-xl font-semibold tracking-tight text-white">$18,240</p>
        <div className="mt-3 flex h-12 items-end gap-1.5">
          {bars.map((h, i) => (
            <span key={i} className={`flex-1 rounded-sm ${i === bars.length - 1 ? "bg-orange-400" : "bg-white/15"}`} style={{ height: `${h}%` }} />
          ))}
        </div>
      </>
    );
  if (kind === "payout")
    return (
      <>
        <div className="flex items-center gap-2 text-xs text-stone-400">
          <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6" aria-hidden="true" className="h-4 w-4 text-rose-300">
            <path d="M3 8h14M5 8V6l5-3 5 3v2M5 8v7M10 8v7M15 8v7M3 16h14" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
          Payout on its way
        </div>
        <p className="mt-2 text-lg font-semibold text-white">$6,180 to Northbank</p>
        <div className="mt-3 h-1.5 overflow-hidden rounded-full bg-white/10">
          <div className="hwr-fill h-full w-full origin-left rounded-full bg-gradient-to-r from-rose-400 to-orange-300" />
        </div>
      </>
    );
  return (
    <div className="flex items-center gap-3">
      <span className="flex h-9 w-9 flex-none items-center justify-center rounded-full bg-gradient-to-br from-orange-300 to-rose-500 text-xs font-semibold text-stone-950">NK</span>
      <div>
        <p className="text-sm font-medium text-white">Nadia Kowal paid you</p>
        <p className="text-xs text-stone-400">$860.00, just now</p>
      </div>
    </div>
  );
}

function FloatCard({ card, index, mx, my, reduce }: { card: (typeof cards)[number]; index: number; mx: MotionValue<number>; my: MotionValue<number>; reduce: boolean }) {
  // Nearer cards (bigger depth) travel further, which is what reads as depth.
  const x = useTransform(mx, (v) => v * card.depth);
  const y = useTransform(my, (v) => v * card.depth);
  return (
    <motion.div className={`absolute ${card.pos}`} style={{ x, y }}>
      <motion.div
        initial={reduce ? false : { opacity: 0, scale: 0.7, y: 60, rotate: 0 }}
        animate={{ opacity: 1, scale: 1, y: 0, rotate: card.rotate }}
        transition={{ type: "spring", stiffness: 90, damping: 14, delay: 0.45 + index * 0.12 }}
      >
        <div className="hwr-drift w-44 rounded-2xl border border-white/10 bg-stone-900/90 p-4 shadow-2xl shadow-black/60 xl:w-56" style={{ animationDuration: `${card.drift}s`, animationDelay: `${-index * 1.7}s` }}>
          <CardBody kind={card.kind} />
        </div>
      </motion.div>
    </motion.div>
  );
}

export default function HeroWithWordRevealAndFloatingCards() {
  const reduce = !!useReducedMotion();
  const ref = useRef<HTMLElement>(null);
  const [onScreen, setOnScreen] = useState(true);
  const rawX = useMotionValue(0);
  const rawY = useMotionValue(0);
  const mx = useSpring(rawX, { stiffness: 50, damping: 18 });
  const my = useSpring(rawY, { stiffness: 50, damping: 18 });

  useEffect(() => {
    const el = ref.current;
    if (!el || typeof IntersectionObserver === "undefined") return;
    const io = new IntersectionObserver(([e]) => setOnScreen(e.isIntersecting), { threshold: 0.05 });
    io.observe(el);
    return () => io.disconnect();
  }, []);

  const onMove = (e: React.PointerEvent<HTMLElement>) => {
    if (reduce) return;
    const r = e.currentTarget.getBoundingClientRect();
    rawX.set((e.clientX - r.left) / r.width - 0.5);
    rawY.set((e.clientY - r.top) / r.height - 0.5);
  };
  const onLeave = () => {
    rawX.set(0);
    rawY.set(0);
  };

  const words = headline.split(" ");
  const ring = "focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-300 focus-visible:ring-offset-2 focus-visible:ring-offset-stone-950";

  return (
    <section ref={ref} data-paused={!onScreen} onPointerMove={onMove} onPointerLeave={onLeave} className="hwr-root relative overflow-hidden bg-stone-950 px-6 py-20 text-white sm:py-24 xl:flex xl:min-h-[700px] xl:items-center">
      <style>{`
        @keyframes hero-with-word-reveal-and-floating-cards-drift { 0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); } 50% { transform: translate3d(6px, -16px, 0) rotate(1.5deg); } }
        @keyframes hero-with-word-reveal-and-floating-cards-fill { 0% { transform: scaleX(0.08); } 70%, 100% { transform: scaleX(1); } }
        @keyframes hero-with-word-reveal-and-floating-cards-spin { to { transform: rotate(360deg); } }
        @keyframes hero-with-word-reveal-and-floating-cards-halo { 0%, 100% { opacity: 0.55; transform: scale(1); } 50% { opacity: 0.95; transform: scale(1.12); } }
        @keyframes hero-with-word-reveal-and-floating-cards-wander { 0%, 100% { transform: translate3d(0, 0, 0); } 50% { transform: translate3d(60px, 30px, 0); } }
        .hwr-drift { animation: hero-with-word-reveal-and-floating-cards-drift 8s ease-in-out infinite; will-change: transform; }
        .hwr-fill { animation: hero-with-word-reveal-and-floating-cards-fill 4.5s cubic-bezier(0.3, 0.7, 0.2, 1) infinite; }
        .hwr-spin { animation: hero-with-word-reveal-and-floating-cards-spin 3.5s linear infinite; }
        .hwr-halo { animation: hero-with-word-reveal-and-floating-cards-halo 2.8s ease-in-out infinite; }
        .hwr-wander { animation: hero-with-word-reveal-and-floating-cards-wander 14s ease-in-out infinite; }
        .hwr-root[data-paused="true"] * { animation-play-state: paused !important; }
        @media (prefers-reduced-motion: reduce) { .hwr-drift, .hwr-fill, .hwr-spin, .hwr-halo, .hwr-wander { animation: none; } }
      `}</style>

      {/* Atmosphere: two slow glows and a dot grid that fades out toward the edges. */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0">
        <div className="hwr-wander absolute -left-24 top-10 h-96 w-96 rounded-full bg-rose-600/25 blur-3xl" />
        <div className="hwr-wander absolute -right-24 bottom-0 h-96 w-96 rounded-full bg-orange-500/20 blur-3xl [animation-delay:-7s]" />
        <div className="absolute inset-0 bg-[radial-gradient(circle,rgba(255,255,255,0.09)_1px,transparent_1px)] [background-size:26px_26px] [mask-image:radial-gradient(ellipse_at_center,black_30%,transparent_75%)]" />
      </div>

      <div className="relative z-10 mx-auto w-full max-w-2xl text-center">
        <motion.p
          initial={reduce ? false : { opacity: 0, y: 12 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.5 }}
          className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3.5 py-1.5 text-sm text-stone-300"
        >
          <span className="h-1.5 w-1.5 rounded-full bg-orange-400" />
          Same-day payouts are now live
        </motion.p>

        <h1 className="mt-6 text-5xl font-semibold leading-none tracking-tight sm:text-7xl">
          <span className="sr-only">{headline}</span>
          <span aria-hidden="true">
            {words.map((w, i) => (
              <span key={i} className="-mb-3 mr-3 inline-block overflow-hidden pb-3 align-bottom last:mr-0 sm:mr-5">
                <motion.span
                  className={`inline-block ${i >= accentFrom ? "text-orange-300" : ""}`}
                  initial={reduce ? false : { y: "115%", opacity: 0, filter: "blur(10px)" }}
                  animate={{ y: "0%", opacity: 1, filter: "blur(0px)" }}
                  transition={{ type: "spring", stiffness: 110, damping: 17, delay: 0.12 + i * 0.075 }}
                >
                  {w}
                </motion.span>
              </span>
            ))}
          </span>
        </h1>

        <motion.div initial={reduce ? false : { opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, delay: 0.75 }}>
          <p className="mx-auto mt-6 max-w-lg text-lg leading-relaxed text-stone-300">
            Marlo turns a finished job into a paid invoice. Clients pay by card or bank transfer in two taps, and the money lands in your account the same day.
          </p>
          <div className="mt-9 flex flex-col items-center justify-center gap-4 sm:flex-row">
            <span className="relative inline-flex">
              <span aria-hidden="true" className="hwr-halo absolute -inset-2 rounded-full bg-gradient-to-r from-orange-500 via-rose-500 to-orange-400 blur-xl" />
              <a href="#" className={`group relative inline-flex overflow-hidden rounded-full p-px ${ring}`}>
                <span aria-hidden="true" className="hwr-spin absolute -inset-full bg-[conic-gradient(from_0deg,#fb923c,#fff7ed,#f43f5e,#fb923c)]" />
                <span className="relative inline-flex items-center gap-2 rounded-full bg-stone-950 px-7 py-3.5 text-sm font-semibold text-white transition group-hover:bg-stone-900">
                  Send your first invoice
                  <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" className="h-4 w-4 transition-transform group-hover:translate-x-0.5">
                    <path fillRule="evenodd" d="M3 10a.75.75 0 0 1 .75-.75h10.64l-3.2-3.2a.75.75 0 1 1 1.06-1.06l4.5 4.5a.75.75 0 0 1 0 1.06l-4.5 4.5a.75.75 0 1 1-1.06-1.06l3.2-3.2H3.75A.75.75 0 0 1 3 10Z" clipRule="evenodd" />
                  </svg>
                </span>
              </a>
            </span>
            <a href="#" className={`inline-flex items-center justify-center rounded-full px-5 py-3.5 text-sm font-semibold text-stone-200 transition hover:text-white ${ring}`}>
              See how payouts work
            </a>
          </div>
          <p className="mt-6 text-sm text-stone-400">Free for your first 10 invoices. No card needed.</p>
        </motion.div>
      </div>

      {/* Below xl the cards sit in a cluster under the text; from xl they float around it. */}
      <div aria-hidden="true" className="pointer-events-none relative mx-auto mt-14 h-64 w-full max-w-sm xl:absolute xl:inset-0 xl:mt-0 xl:h-auto xl:max-w-none">
        {cards.map((c, i) => (
          <FloatCard key={c.kind} card={c} index={i} mx={mx} my={my} reduce={reduce} />
        ))}
      </div>
    </section>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/hero-with-word-reveal-and-floating-cards.json

When to use it

How to customize it

Accessibility

Questions

How does the word by word reveal work?

The headline string is split on spaces. Each word sits in a wrapper with `overflow-hidden`, and the word inside starts pushed down below that wrapper, transparent and blurred. A spring then moves it to its resting place. Because every word has a slightly later `delay`, they arrive in order.

How do the cards float and follow the mouse at the same time?

Each card is three nested elements. The outer one is moved by the pointer through two spring values, the middle one handles the entrance and the tilt, and the inner one runs a slow CSS keyframe loop. Keeping the three movements on separate elements stops them from overwriting each other's transform.

How is the glowing button made?

A wide conic gradient spins behind a button that has one pixel of padding, so only a thin moving edge shows around the dark inner pill. A second, blurred gradient sits behind the whole button and pulses in opacity and scale.

Will this hurt page speed?

The animation only changes `transform`, `opacity` and `filter` on a handful of elements, so the browser can keep it on the compositor. The loops pause when the hero scrolls out of view.

Can I use it on a light background?

Yes. Change `bg-stone-950` on the section and the inner button pill, switch the card classes to a white background with a light border, and darken the text colours. The motion code does not depend on the colours.

Related: Dark Hero with Spotlight Sweep and Line Reveal, Hero with Live Updating Dashboard Mock, Dark Hero with Gradient Glow and Product Window.

More hero sections or all free UI components. Start for free to have a full site built around it.

More in Hero Sections

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap