Card with Diagonal Glare Sweep on Hover

Three membership cards in the shape of a bank card: a dark day pass, a green monthly pass and a gold yearly pass. When you point at one, or tab to it, a streak of light crosses it from corner to corner, like a laminated card catching the sun, and the card lifts slightly. The streak is a single pseudo-element with a gradient, so any block can use it. The demo sweeps each card in turn until you touch one. On phones the cards stack.

Code

// Adapted from Magic UI (https://magicui.design/docs/components/glare-hover)
// Copyright (c) Magic UI. MIT License: https://we.inc/components/licenses
import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
import { Mountain } from "lucide-react";

const passes = [
  { id: "day", name: "Day pass", price: "$18", terms: "One visit, any hall", number: "GH 0412 7730", holder: "Guest", surface: "bg-gradient-to-br from-zinc-700 via-zinc-900 to-black text-white", glare: "rgba(255,255,255,0.5)", accent: "text-zinc-300" },
  { id: "month", name: "Monthly", price: "$64", terms: "Unlimited climbs, one guest a week", number: "GH 2208 1954", holder: "Priya Raman", surface: "bg-gradient-to-br from-emerald-500 via-emerald-700 to-teal-950 text-white", glare: "rgba(209,250,229,0.55)", accent: "text-emerald-100" },
  { id: "year", name: "Founder year", price: "$590", terms: "Every hall, every class, gear hire", number: "GH 0001 0086", holder: "Joss Whitlow", surface: "bg-gradient-to-br from-amber-200 via-amber-400 to-orange-600 text-amber-950", glare: "rgba(255,255,255,0.9)", accent: "text-amber-900" },
];

/**
 * Wraps a card and sends a diagonal streak of light across it on hover or
 * focus. `on` plays it from code. `color` is the streak, `angle` its slant,
 * `duration` the sweep time in milliseconds.
 */
function GlareCard({ children, className = "", color = "rgba(255,255,255,0.5)", angle = -45, duration = 1000, on = false }: { children: ReactNode; className?: string; color?: string; angle?: number; duration?: number; on?: boolean }) {
  const vars = {
    "--card-with-diagonal-glare-sweep-on-hover-color": color,
    "--card-with-diagonal-glare-sweep-on-hover-angle": `${angle}deg`,
    "--card-with-diagonal-glare-sweep-on-hover-time": `${duration}ms`,
  } as CSSProperties;
  return (
    <a href="#" data-on={on} style={vars} className={`card-with-diagonal-glare-sweep-on-hover-glare relative block overflow-hidden transition-transform duration-300 hover:-translate-y-1.5 focus:outline-none focus-visible:ring-2 focus-visible:ring-stone-900 focus-visible:ring-offset-4 focus-visible:ring-offset-stone-200 motion-reduce:transition-none ${className}`}>
      {children}
    </a>
  );
}

export default function CardWithDiagonalGlareSweepOnHover() {
  // Until someone points at a card, sweep each one in turn.
  const [demo, setDemo] = useState(-1);
  const [touched, setTouched] = useState(false);

  useEffect(() => {
    if (touched) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    let i = 0;
    let off = 0;
    const play = () => {
      setDemo(i % passes.length);
      i += 1;
      off = window.setTimeout(() => setDemo(-1), 1000);
    };
    const first = window.setTimeout(play, 100);
    const timer = window.setInterval(play, 1300);
    return () => {
      window.clearTimeout(first);
      window.clearTimeout(off);
      window.clearInterval(timer);
    };
  }, [touched]);

  return (
    <section className="bg-stone-200 px-5 py-16 sm:px-6 sm:py-24">
      <style>{`
        @keyframes card-with-diagonal-glare-sweep-on-hover-sweep {
          from { background-position: -45% -45%; }
          to { background-position: 70% 70%; }
        }
        .card-with-diagonal-glare-sweep-on-hover-glare::before {
          content: "";
          position: absolute;
          inset: 0;
          z-index: 10;
          pointer-events: none;
          background-repeat: no-repeat;
          background-image: linear-gradient(var(--card-with-diagonal-glare-sweep-on-hover-angle), transparent 60%, var(--card-with-diagonal-glare-sweep-on-hover-color) 70%, transparent, transparent 100%);
          background-size: 250% 250%;
          background-position: -100% -100%;
        }
        .card-with-diagonal-glare-sweep-on-hover-glare:hover::before,
        .card-with-diagonal-glare-sweep-on-hover-glare:focus-visible::before,
        .card-with-diagonal-glare-sweep-on-hover-glare[data-on="true"]::before {
          animation: card-with-diagonal-glare-sweep-on-hover-sweep var(--card-with-diagonal-glare-sweep-on-hover-time) ease-in-out both;
        }
        @media (prefers-reduced-motion: reduce) {
          .card-with-diagonal-glare-sweep-on-hover-glare::before { display: none; }
        }
      `}</style>

      <div className="mx-auto max-w-6xl">
        <div className="max-w-2xl">
          <p className="text-sm font-semibold uppercase tracking-widest text-stone-600">Granite Hall climbing</p>
          <h2 className="mt-3 text-3xl font-semibold tracking-tight text-stone-900 sm:text-5xl">Pick a pass. Chalk up.</h2>
          <p className="mt-4 text-base leading-7 text-stone-600">Three halls, 240 routes reset every fortnight, and a card that gets you through the gate. Point at one to catch the light.</p>
        </div>

        <ul className="mt-12 grid gap-6 md:grid-cols-3" onPointerEnter={() => setTouched(true)} onFocus={() => setTouched(true)}>
          {passes.map((p, i) => (
            <li key={p.id}>
              <GlareCard on={demo === i} color={p.glare} className={`rounded-3xl p-6 shadow-xl shadow-stone-900/20 ${p.surface}`}>
                <div className="flex aspect-[8/5] flex-col justify-between">
                  <div className="flex items-start justify-between">
                    <span className="flex items-center gap-2 text-sm font-semibold uppercase tracking-widest">
                      <Mountain aria-hidden="true" className="h-5 w-5" />
                      Granite Hall
                    </span>
                    <span className="text-2xl font-semibold tracking-tight">{p.price}</span>
                  </div>
                  <div>
                    <p className="text-2xl font-semibold tracking-tight">{p.name}</p>
                    <p className={`mt-1 text-sm ${p.accent}`}>{p.terms}</p>
                  </div>
                  <div className={`flex items-end justify-between font-mono text-xs uppercase tracking-widest ${p.accent}`}>
                    <span>{p.number}</span>
                    <span>{p.holder}</span>
                  </div>
                </div>
              </GlareCard>
            </li>
          ))}
        </ul>
      </div>
    </section>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/card-with-diagonal-glare-sweep-on-hover.json

When to use it

How to customize it

Accessibility

Questions

How does the glare effect work?

A `::before` element covers the card and has a linear gradient that is transparent except for one narrow band. The gradient is two and a half times the size of the card and starts positioned off one corner. On hover a keyframe moves its background position to the opposite corner, carrying the band across.

Why does the light not sweep back when I leave?

The sweep is a keyframe animation that is only applied while the card is hovered or focused. When hover ends the animation is removed and the gradient returns to its hidden starting position at once, so each hover plays one clean pass.

Does it work on touch screens?

A tap gives the card focus on most devices, which plays the sweep once. You can also play it from code by passing `on`, which is how the demo runs by itself.

Can I put it on a button or an image?

Yes. The element needs `relative` and `overflow-hidden`, which `GlareCard` adds. Give it the same corner radius as its content so the streak is clipped to the shape.

Is it expensive to animate?

It animates the background position of one layer, for under a second, on the element you are pointing at. That is light enough to use on a whole grid of cards.

Adapted from Glare Hover in Magic UI, used under its open-source licence. Licences and credits

Related: 3D Tilt Card with Parallax Layers and Glare, Spotlight Cards with Pointer Light, Card with Travelling Border Beam.

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

More in Animated Cards

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