Skip to content
Black FridayBlack Friday launch price: Lifetime $149 until 30 Nov. Regular price $299 from 1 Dec.ends in 15d 03:13:41Get Lifetime →
loopkit
freeNew138 lines · framer-motion

Card Spotlight Grid

A spotlight that only ever lights the border. A grid of cards whose 1px border lights up in a radial spotlight that follows the cursor, built with a masked gradient so the glow never leaks into the card body.

Use it for
A features, integrations or tool grid where you want a premium cursor-reactive feel without a glow that washes out the copy inside each card.
Themes
Light + dark
License
Commercial use
Live

Building blocks

Cards that light up where you point

Instant context

Every tool call carries the thread it came from, so nothing needs re-explaining.

Scoped access

Each card the agent can draw gets its own narrow set of permissions.

Composable runs

Chain cards into a pipeline without writing glue code between them.

Always watched

A quiet trace runs underneath, ready the moment something looks off.

tsx
"use client";

import * as React from "react";
import { motion, useMotionTemplate, useMotionValue, useReducedMotion } from "framer-motion";
import { cn } from "@/lib/utils";

function useReducedMotionSafe() {
  const prefersReduced = useReducedMotion();
  const [mounted, setMounted] = React.useState(false);
  React.useEffect(() => setMounted(true), []);
  return mounted ? !!prefersReduced : false;
}

export interface SpotlightCardItem {
  title: string;
  description: string;
  icon?: "bolt" | "shield" | "layers" | "radar";
}

export interface CardSpotlightGridProps {
  eyebrow?: string;
  title?: string;
  cards?: SpotlightCardItem[];
  /** Colour of the cursor spotlight on the card borders. */
  spotlightColor?: "signal" | "info" | "lime" | "ok";
  /** Radius in px of the spotlight. */
  radius?: number;
  columns?: 2 | 3;
  className?: string;
}

const DEFAULT_CARDS: SpotlightCardItem[] = [
  { title: "Instant context", description: "Every tool call carries the thread it came from, so nothing needs re-explaining.", icon: "bolt" },
  { title: "Scoped access", description: "Each card the agent can draw gets its own narrow set of permissions.", icon: "shield" },
  { title: "Composable runs", description: "Chain cards into a pipeline without writing glue code between them.", icon: "layers" },
  { title: "Always watched", description: "A quiet trace runs underneath, ready the moment something looks off.", icon: "radar" },
];

const colorVar: Record<NonNullable<CardSpotlightGridProps["spotlightColor"]>, string> = {
  signal: "var(--signal)",
  info: "var(--info)",
  lime: "var(--lime)",
  ok: "var(--ok)",
};

function Icon({ name }: { name: NonNullable<SpotlightCardItem["icon"]> }) {
  const common = { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round" as const, strokeLinejoin: "round" as const, className: "size-5" };
  switch (name) {
    case "bolt":
      return (
        <svg {...common}>
          <path d="M13 2 4 14h6l-1 8 9-12h-6l1-8Z" />
        </svg>
      );
    case "shield":
      return (
        <svg {...common}>
          <path d="M12 3 4 6v6c0 5 3.5 7.5 8 9 4.5-1.5 8-4 8-9V6l-8-3Z" />
        </svg>
      );
    case "layers":
      return (
        <svg {...common}>
          <path d="m12 3 9 5-9 5-9-5 9-5Z" />
          <path d="m3 13 9 5 9-5" />
        </svg>
      );
    case "radar":
      return (
        <svg {...common}>
          <circle cx="12" cy="12" r="9" />
          <path d="M12 12 18 7" />
          <circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none" />
        </svg>
      );
  }
}

function SpotlightCard({ card, color, radius }: { card: SpotlightCardItem; color: string; radius: number }) {
  const reduce = useReducedMotionSafe();
  const mx = useMotionValue(50);
  const my = useMotionValue(50);
  const [active, setActive] = React.useState(false);
  const background = useMotionTemplate`radial-gradient(${radius}px circle at ${mx}% ${my}%, ${color}, transparent 72%)`;

  return (
    <div
      className="group relative rounded-3xl bg-border p-px transition-colors"
      onPointerMove={(e) => {
        if (reduce) return;
        const r = e.currentTarget.getBoundingClientRect();
        mx.set(((e.clientX - r.left) / r.width) * 100);
        my.set(((e.clientY - r.top) / r.height) * 100);
      }}
      onPointerEnter={() => setActive(true)}
      onPointerLeave={() => setActive(false)}
    >
      <motion.div aria-hidden className="pointer-events-none absolute inset-0 rounded-3xl" style={{ background }} animate={{ opacity: active && !reduce ? 1 : 0 }} transition={{ duration: 0.25 }} />
      <div className="relative z-10 h-full rounded-[calc(1.5rem-1px)] bg-card p-6">
        <span className="inline-flex size-10 items-center justify-center rounded-xl bg-muted text-foreground transition-colors duration-300 group-hover:bg-signal/10 group-hover:text-signal">
          {card.icon && <Icon name={card.icon} />}
        </span>
        <h3 className="mt-4 font-display text-lg font-bold tracking-tight text-foreground">{card.title}</h3>
        <p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">{card.description}</p>
      </div>
    </div>
  );
}

const colsClass: Record<2 | 3, string> = { 2: "sm:grid-cols-2", 3: "sm:grid-cols-2 lg:grid-cols-4" };

export function CardSpotlightGrid({
  eyebrow = "Building blocks",
  title = "Cards that light up where you point",
  cards = DEFAULT_CARDS,
  spotlightColor = "signal",
  radius = 180,
  columns = 3,
  className,
}: CardSpotlightGridProps) {
  const color = colorVar[spotlightColor];
  return (
    <section className={cn("w-full bg-background px-4 py-20 sm:px-6 lg:px-10", className)}>
      <div className="mx-auto max-w-3xl text-center">
        <p className="font-mono text-[11px] uppercase tracking-[0.2em] text-muted-foreground">{eyebrow}</p>
        <h2 className="mt-3 font-display text-[clamp(2rem,5vw,3.5rem)] font-extrabold leading-[0.95] tracking-[-0.03em] text-foreground">{title}</h2>
      </div>
      <div className={cn("mx-auto mt-12 grid max-w-6xl grid-cols-1 gap-4", colsClass[columns])}>
        {cards.map((c) => (
          <SpotlightCard key={c.title} card={c} color={color} radius={radius} />
        ))}
      </div>
    </section>
  );
}

export default CardSpotlightGrid;

How it behaves

  • →Each card tracks pointer position locally and renders a radial-gradient spotlight behind a 1px padding ring, masked so only the border shows the glow (the gradient sits beneath an inner card that covers everything but that ring).
  • →The spotlight fades in and out with opacity on pointer enter/leave instead of popping.
  • →spotlightColor and radius are passed straight into the gradient so the effect can match any accent token.
  • →Pointer tracking is skipped entirely under prefers-reduced-motion, leaving a plain bordered card.

Accessibility

  • ✓The spotlight layer is `aria-hidden` and `pointer-events-none`; it never sits between the pointer and real content.
  • ✓Icons are decorative (`aria-hidden`); card titles are real headings read by screen readers in document order.
  • ✓Honours prefers-reduced-motion.