Skip to content
ARDENT / MOTION LAB
Library

Microinteractions · Starter

Spotlight Card

Pointer position is written to CSS custom properties; a radial gradient reads them. No React re-renders on move.

Live preview

Move a mouse across the cards, or Tab between them.

Copy-ready · type-checkedThe preview above renders this exact file. Reduced-motion fallback included.
/**
 * Spotlight Card
 * A soft theme-token spotlight follows attention across three cards.
 * Requires: motion, Tailwind with shadcn semantic tokens.
 */
import { useReducedMotion } from "motion/react";

export default function SpotlightCard({
  reduced: reducedProp,
  intensity = 1,
}: {
  reduced?: boolean;
  intensity?: number;
}) {
  const systemReduced = useReducedMotion();
  const reduced = reducedProp ?? systemReduced ?? false;
  const amount = Math.min(1.5, Math.max(0.5, intensity));

  return (
    <section className="flex min-h-[80vh] items-center justify-center bg-background px-6 py-20 text-foreground">
      <style>{`
        .ardent-spotlight {
          background: radial-gradient(circle 260px at var(--spot-x, 50%) var(--spot-y, 50%), var(--foreground), transparent 75%);
        }
      `}</style>
      <div className="w-full max-w-6xl">
        <p className="mb-6 font-mono text-xs uppercase tracking-[0.2em] text-muted-foreground">
          Where attention goes / 04
        </p>
        <h2 className="mb-10 font-serif text-5xl tracking-tight">Considered from every angle.</h2>
        <div className="grid gap-4 md:grid-cols-3">
          {[
            ["Strategy", "Find the idea worth building around."],
            ["Identity", "Make the essential unmistakable."],
            ["Motion", "Give every interaction intention."],
          ].map(([title, description], index) => (
            <article
              key={title}
              tabIndex={0}
              onFocus={(event) => {
                event.currentTarget.style.setProperty("--spot-x", "50%");
                event.currentTarget.style.setProperty("--spot-y", "50%");
              }}
              onPointerMove={(event) => {
                if (
                  reduced ||
                  event.pointerType !== "mouse" ||
                  !window.matchMedia("(hover: hover) and (pointer: fine)").matches
                )
                  return;
                const rect = event.currentTarget.getBoundingClientRect();
                event.currentTarget.style.setProperty("--spot-x", `${event.clientX - rect.left}px`);
                event.currentTarget.style.setProperty("--spot-y", `${event.clientY - rect.top}px`);
              }}
              className="group relative isolate overflow-hidden rounded-2xl border border-border bg-card p-8 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4"
            >
              {!reduced && (
                <div className="pointer-events-none absolute inset-0 -z-10 opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-visible:opacity-100">
                  <div
                    className="ardent-spotlight absolute inset-0"
                    style={{ opacity: 0.09 * amount }}
                  />
                </div>
              )}
              <p className="font-mono text-xs uppercase tracking-[0.2em] text-muted-foreground">
                0{index + 1} / Discipline
              </p>
              <h3 className="mt-24 font-serif text-4xl tracking-tight">{title}</h3>
              <p className="mt-5 text-sm leading-relaxed text-muted-foreground">{description}</p>
            </article>
          ))}
        </div>
      </div>
    </section>
  );
}
Next patternBlend-Mode Cursor→