Next.js hover card: Tailwind scale, shadow, and spotlight effects

Published: September 12, 2026 · 6 min read

Add hover animation to Next.js feature and project cards three ways: Tailwind-only scale and shadow, a cursor-following spotlight, and Framer Motion whileHover.

Next.js hover cards that feel too quiet or too busy

You lay out a grid of feature or project cards in Next.js and it feels too quiet — nothing says "click me." Then you add scale, a big shadow, and an image zoom all at once, and the page starts to feel busy.

A hover card looks right when exactly one thing reacts. Pick the border color, the image zoom, or a small lift.

Keep the duration at 150–300ms with an ease-out curve. If you scale, stay around 1.02–1.04.

This guide walks through the code for:

  • A Tailwind CSS-only card whose image zooms on hover

  • A spotlight card that glows where the cursor is

  • A lift effect with Motion (formerly Framer Motion) whileHover

  • What to watch for on touch devices and keyboard navigation

3 reasons a hover card should react in one place

A hover effect exists to say "this is clickable." When several things move at once, the eye has no clear place to land. In a features section with six to nine cards, the smaller each card's motion, the calmer the whole grid looks.

The second reason is layout. Changing width, margin, or border thickness on hover pushes the neighboring cards around. If you only touch transform and opacity, nothing else in the layout moves, and the browser has less work to do.

The third reason is touch. Mobile browsers apply the hover state on tap and can leave it there until you tap elsewhere ("sticky hover").

Any description or button that only appears on hover simply does not exist on mobile. Keep information visible by default and treat hover as decoration.

Building a hover card with Tailwind CSS only

Start with the version that needs no extra packages. It can stay a Server Component — no "use client" required. The whole card is a Link, and only the image scales inside an overflow-hidden frame.

components/WorkCard.tsx
import Image from "next/image";
  import Link from "next/link";

  type WorkCardProps = {
    href: string;
    title: string;
    summary: string;
    image: string;
  };

  export function WorkCard({ href, title, summary, image }: WorkCardProps) {
    return (
      <Link
        href={href}
        className="group block overflow-hidden rounded-2xl border border-zinc-200 bg-white transition-colors duration-200 ease-out hover:border-zinc-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-900 focus-visible:ring-offset-2"
      >
        <div className="relative aspect-[4/3] overflow-hidden bg-zinc-100">
          <Image
            src={image}
            alt=""
            fill
            sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
            className="object-cover transition-transform duration-300 ease-out group-hover:scale-[1.04] group-focus-visible:scale-[1.04] motion-reduce:transition-none"
          />
        </div>
        <div className="p-5">
          <h3 className="font-semibold text-zinc-950">{title}</h3>
          <p className="mt-2 text-sm leading-relaxed text-zinc-600">{summary}</p>
        </div>
      </Link>
    );
  }

Zoom the image to keep the hover card calm

Scaling the whole card with hover:scale grows the radius and shadow too, and the card starts to overlap its neighbors. Zooming only the image inside its frame keeps the card's outline still. With group and group-hover, the image reacts wherever the pointer is on the card.

If the image just restates the title, leave alt empty; if it carries its own information, describe it. Either way, keep the alt attribute itself.

A lift-and-shadow hover card for text-only cards

For cards without images, a small lift plus a shadow works well. About 4px of movement is plenty. Stop at roughly shadow-lg and skip large colored glows.

tsx
<div className="rounded-2xl border border-zinc-200 bg-white p-6 transition duration-200 ease-out hover:-translate-y-1 hover:shadow-lg motion-reduce:transition-none motion-reduce:hover:translate-y-0">
    <h3 className="font-semibold">Read invoices automatically</h3>
    <p className="mt-2 text-sm text-zinc-600">Upload a PDF and get the amount and due date back.</p>
  </div>

Give keyboard users the same emphasis

People moving with the Tab key never see your hover effect. Put the same emphasis on focus-visible and the card reads as selectable for mouse and keyboard alike. The code above applies both the ring and the image zoom on focus-visible.

In Tailwind CSS v4, hover: styles only apply on devices that can actually hover. On a v3 project, a tapped card can keep its hover state on phones. Adding hoverOnlyWhenSupported: true under future in tailwind.config gives you the v4 behavior.

A spotlight hover card that follows the cursor

This is the soft glow that tracks the cursor, common on dark feature sections. Reading pointer coordinates means it has to be a Client Component.

The key is not to store the coordinates in useState. Updating it re-renders on every pointer move. Writing CSS variables (--x and --y) straight onto the element moves the glow without any React re-render.

components/SpotlightCard.tsx
"use client";

  import { useRef, type PointerEvent, type ReactNode } from "react";

  export function SpotlightCard({ children }: { children: ReactNode }) {
    const ref = useRef<HTMLDivElement>(null);

    function handlePointerMove(event: PointerEvent<HTMLDivElement>) {
      // Skip touch and pen input
      if (event.pointerType !== "mouse" || !ref.current) return;
      const rect = ref.current.getBoundingClientRect();
      ref.current.style.setProperty("--x", `${event.clientX - rect.left}px`);
      ref.current.style.setProperty("--y", `${event.clientY - rect.top}px`);
    }

    return (
      <div
        ref={ref}
        onPointerMove={handlePointerMove}
        className="group relative overflow-hidden rounded-2xl border border-white/10 bg-zinc-900 p-6 text-white"
      >
        <div
          aria-hidden="true"
          className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
          style={{
            background:
              "radial-gradient(240px circle at var(--x, 50%) var(--y, 50%), rgba(255, 255, 255, 0.12), transparent 70%)",
          }}
        />
        <div className="relative">{children}</div>
      </div>
    );
  }

Use the spotlight in one section only

It draws the eye, so putting it on every card on the page gets tiring. One features section, or just the highlighted plan in a pricing table, is a good limit.

Phones have no mouse, so the handler ignores any pointerType other than mouse. Make sure the border and text contrast already work without the glow, so nothing depends on it.

A hover card that lifts softly with Framer Motion

If the project already uses Motion, whileHover and whileTap give you a springy feel. The motion settles more softly than a CSS transition. Setup is covered in https://design-layer.com/en/articles/nextjs-framer-motion-guide.

components/LiftCard.tsx
"use client";

  import { motion } from "motion/react";
  import type { ReactNode } from "react";

  export function LiftCard({ children }: { children: ReactNode }) {
    return (
      <motion.div
        whileHover={{ y: -4 }}
        whileTap={{ scale: 0.98 }}
        transition={{ type: "spring", stiffness: 300, damping: 24 }}
        className="rounded-2xl border border-zinc-200 bg-white p-6"
      >
        {children}
      </motion.div>
    );
  }

Build the hover card in CSS when CSS is enough

You don't need to add Motion just for a hover lift. It earns its place when the cards also animate in on scroll or move when reordered.

Common hover card mistakes and how to fix them

Check these before you ship:

  • transition-all recalculating colors and shadows every time (list only the properties you change)

  • The whole card is a Link but contains another link or button (an a cannot contain an a, and the target becomes unclear)

  • An overflow-hidden parent clipping the shadow of a lifted card

  • Descriptions or prices that only show on hover

  • A scale factor so large the image edges turn soft

If the card needs an action besides "view details," stop making the whole card a link and link only the title. To keep a large click area, add after:absolute after:inset-0 to the title link and make the card relative.

Copying a section with hover cards already tuned

The Moonsworth Companies Grid page on DesignLayer: check the live preview on the left, then copy the source with Copy for Cursor on the right
The Moonsworth Companies Grid page on DesignLayer: check the live preview on the left, then copy the source with Copy for Cursor on the right

The code above gets you one card. A real features section also needs the grid, image crops, heading sizes, and hover strength to match, and that tuning is where the time goes.

DesignLayer is a catalog of human-built React + Tailwind CSS sections with motion. You preview them live in the browser, feel the hover strength and speed yourself, then copy.

Browsing is free and needs no card. https://design-layer.com/en/components

Copy for Cursor copies the integration instructions, the live preview URL, the TSX, and the required npm packages together, ready to hand to Cursor or Claude. Free copies are limited to the 10 Free Sample components with no monthly limit on a free account, plus 1 video prompt a month. The rest of the catalog and MCP are the 30-Day Build Pass ($19 once), Lifetime Access ($79 once), or Plus ($9/month).

If you're unsure how to lay the cards out in the first place, see https://design-layer.com/en/articles/nextjs-features-section-guide for features sections and https://design-layer.com/en/articles/nextjs-bento-grid-guide for mixed-size bento layouts.

Next.js hover card: common questions

  • Not if you build it with Tailwind's hover: and group-hover: only — it can stay a Server Component.

    A spotlight that reads pointer coordinates, or anything using Motion, does need the directive. Even then, make only the card a browser-side component and keep the page on the server.

Summary: building hover cards in Next.js

  • React in one place only: border, image, or lift

  • Change only position, scale, opacity, and color so neighbors never move

  • Give keyboard focus the same emphasis and never hide information behind hover

  • Run the spotlight in the browser and pass coordinates through CSS variables, not state

  • Reach for Motion when the cards have other motion too

Start by dropping in the Tailwind WorkCard and tuning the scale and duration by hand. When you want the whole section to match, try the hover effects live in the DesignLayer catalog before you pick one. https://design-layer.com/en/components

Related guides