How to Make a “Spotlight” Effect on Hover

A hoverable spotlight draws the eye to content without extra markup or heavy images. In this project you will build a clean, flexible “spotlight” effect that fades in on hover, dims the surroundings, and can follow the cursor with one tiny JavaScript helper. You will learn both a pure CSS hover version and a cursor-tracking version, plus enhancements that add polish while keeping performance snappy.

Why a Spotlight Effect on Hover Matters

Hover spotlights improve scannability and reduce visual noise. Instead of throwing motion at every card or button, a single soft circle that reveals content is enough to guide attention. Compared to image overlays, CSS gradients are lighter, themeable, and resolution-independent. You also avoid extra DOM nodes by leaning on pseudo-elements. The effect becomes a reusable utility you can drop into any card, hero, or callout pattern.

Prerequisites

You will get the most from this tutorial if you are comfortable with layout and pseudo-elements. The base version is pure CSS. The cursor-following variant adds a short script that feeds pointer coordinates into CSS variables.

  • Basic HTML
  • CSS custom properties
  • CSS pseudo-elements (::before / ::after)

Step 1: The HTML Structure

The spotlight lives on a card component. Each card uses a single wrapper with text inside. The overlay is created with a pseudo-element, so there is no extra HTML. Here is the complete markup for a small grid of demo cards that all share the same spotlight behavior.


<main class="spotlight-demo" aria-label="Spotlight hover demo">
  <section class="grid">
    <article class="card spotlight">
      <h3>Card One</h3>
      <p>Hover to reveal details with a soft spotlight. This works with any background.</p>
      <button type="button">Learn more</button>
    </article>

    <article class="card spotlight">
      <h3>Card Two</h3>
      <p>A dimmed overlay keeps focus where you aim the light.</p>
      <button type="button">Open</button>
    </article>

    <article class="card spotlight">
      <h3>Card Three</h3>
      <p>Add cursor tracking to have the spotlight follow the pointer.</p>
      <button type="button">Details</button>
    </article>
  </section>
</main>

Step 2: The Basic CSS & Styling

The foundation sets a neutral canvas and defines a few CSS variables for color and sizing. The .grid lays out cards responsively. Each .card is positioned relative so its pseudo-element can cover it. You can theme the overlay strength and spotlight size with variables.

/* CSS */
:root {
  --bg: #0f1226;
  --ink: #e7e9f1;
  --muted: #9aa0b2;

  --card-bg: linear-gradient(135deg, #1e2240, #232952);
  --radius: 14px;

  --spot-size: 120px;       /* radius-ish control via stops */
  --spot-softness: 80px;    /* feathered edge distance */
  --dim-outer: rgba(0, 0, 0, 0.6);
  --dim-inner: rgba(0, 0, 0, 0.35);
  --spot-opacity: 1;        /* allow theming/fading */
}

*,
*::before,
*::after { box-sizing: border-box; }

body {
  margin: 0;
  font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
  color: var(--ink);
  background: radial-gradient(1200px 600px at 50% -200px, #1b1f3b 0, #0f1226 60%) no-repeat var(--bg);
  line-height: 1.5;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 4rem 1rem;
}

.spotlight-demo .grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 24px;
  width: min(1100px, 100%);
}

.card {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius);
  padding: 1.25rem 1.25rem 1.5rem;
  background: var(--card-bg);
  border: 1px solid rgba(255, 255, 255, 0.08);
  min-height: 180px;
}

.card h3 {
  margin: 0 0 0.5rem;
  font-weight: 700;
  letter-spacing: 0.2px;
}

.card p {
  margin: 0 0 1rem;
  color: var(--muted);
}

.card button {
  appearance: none;
  border: 0;
  border-radius: 8px;
  padding: 0.6rem 0.9rem;
  background: #4f65ff;
  color: white;
  font-weight: 600;
  cursor: pointer;
}

Advanced Tip: Make your spotlight themeable. Expose variables like –spot-size, –spot-softness, and –dim-outer so you can reuse the effect on different backgrounds without hunting through multiple selectors.

Step 3: Building the Spotlight Overlay

The overlay is a ::before layer that covers the card. It uses a radial-gradient with a transparent middle and dim edges. On hover, the layer fades in. Without JavaScript, the light will center by default. Later you will wire the spotlight to the cursor with two lines of script.

/* CSS */
.spotlight::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none; /* let clicks pass through */
  opacity: 0;
  transition: opacity 220ms ease;
  /* The gradient "carves" a transparent hole that looks like light */
  background:
    radial-gradient(
      circle at var(--x, 50%) var(--y, 50%),
      rgba(255, 255, 255, 0) 0,
      rgba(255, 255, 255, 0) calc(var(--spot-size) - 20px),
      var(--dim-inner) var(--spot-size),
      var(--dim-outer) calc(var(--spot-size) + var(--spot-softness)),
      var(--dim-outer) 100%
    );
  /* The whole layer can be faded via --spot-opacity for theming */
  filter: opacity(var(--spot-opacity));
}

.spotlight:hover::before,
.spotlight:focus-within::before {
  opacity: 1;
}

How This Works (Code Breakdown)

Setting position: relative on .card earlier establishes a positioning context for ::before. The pseudo-element stretches across the card with inset: 0 and does not block input because pointer-events is set to none. That avoids accidental hover flicker on buttons and links inside the card.

The star of the show is the radial-gradient. The keyword circle defines a circular gradient. If you want a refresher on round shape foundations, this pairs well with how to make a circle with CSS. The gradient’s center uses at var(–x, 50%) var(–y, 50%). Without JavaScript the fallback values keep the light in the center. The color stops create a transparent inner zone that reveals the card content, then blend into semi-opaque dimming colors toward the edges.

The stops use variables to keep sizing flexible. –spot-size is the main radius, and –spot-softness defines the feather distance. By adjusting those two values per component, you can create tiny spotlights for buttons or broad lights for hero sections. The opacity transition gives a subtle fade on hover and focus-within so keyboard users get the same visual treatment.

The container itself is a rectangle, which you can relate to the building blocks in this shape library on how to make a rectangle with CSS. The spotlight simply reveals part of that rectangular card.

Step 4: Make the Spotlight Follow the Cursor

To move the light with the pointer, map mouse coordinates to CSS variables. You update –x and –y on pointermove so the radial-gradient’s center tracks the cursor. This is a short script with no dependencies.

// JS
const cards = document.querySelectorAll('.card.spotlight');

cards.forEach((card) => {
  card.addEventListener('pointermove', (e) => {
    const rect = card.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    card.style.setProperty('--x', `${x}px`);
    card.style.setProperty('--y', `${y}px`);
  });

  card.addEventListener('pointerleave', () => {
    /* Reset to center on exit for a graceful fade */
    card.style.setProperty('--x', '50%');
    card.style.setProperty('--y', '50%');
  });
});

How This Works (Code Breakdown)

getBoundingClientRect gives you the card’s top-left corner. Subtracting that from the clientX and clientY pointer position yields coordinates relative to the card. Those values are applied to –x and –y on the element, which the gradient already reads as its center. The spotlight lerps visually because the gradient center updates every pointermove event. The pointerleave handler resets the center to 50% so the effect recenters when the user exits the card.

If you plan to add the effect to a search box or a product “zoom” area, pairing it with a visual cue helps. For example, a small icon next to the title can hint at interactivity. For a pure CSS option, you can make a magnifying glass icon with CSS and position it in the corner of the card.

Advanced Techniques: Adding Animations & Hover Effects

You can push the effect further with a glow ring, a soft color tint, or a gentle entrance animation. The examples below keep the same HTML and only extend CSS.

/* CSS */
/* 1) Add a glow ring inside the spotlight */
.spotlight.glow::before {
  background:
    radial-gradient(
      circle at var(--x, 50%) var(--y, 50%),
      rgba(255, 255, 255, 0.10) 0,
      rgba(255, 255, 255, 0.06) calc(var(--spot-size) - 32px),
      rgba(255, 255, 255, 0.24) calc(var(--spot-size) - 28px),
      rgba(255, 255, 255, 0.00) var(--spot-size)
    ),
    radial-gradient(
      circle at var(--x, 50%) var(--y, 50%),
      rgba(255, 255, 255, 0) 0,
      rgba(255, 255, 255, 0) calc(var(--spot-size) - 20px),
      var(--dim-inner) var(--spot-size),
      var(--dim-outer) calc(var(--spot-size) + var(--spot-softness)),
      var(--dim-outer) 100%
    );
}

/* 2) Gentle "pop-in" of the light size on hover */
@keyframes spot-pop {
  0% {
    --spot-size: 0px;
  }
  100% {
    /* The card's default variable value will take over at 100% */
  }
}

.spotlight.pop:hover::before,
.spotlight.pop:focus-within::before {
  animation: spot-pop 220ms ease-out;
}

/* 3) Optional color tint in the center */
.spotlight.tinted::before {
  background:
    radial-gradient(
      circle at var(--x, 50%) var(--y, 50%),
      rgba(79, 101, 255, 0.14) 0,
      rgba(79, 101, 255, 0.00) calc(var(--spot-size) - 8px)
    ),
    radial-gradient(
      circle at var(--x, 50%) var(--y, 50%),
      rgba(255, 255, 255, 0) 0,
      rgba(255, 255, 255, 0) calc(var(--spot-size) - 20px),
      var(--dim-inner) var(--spot-size),
      var(--dim-outer) calc(var(--spot-size) + var(--spot-softness)),
      var(--dim-outer) 100%
    );
}

/* Reduced motion: keep the hover, remove the pop animation */
@media (prefers-reduced-motion: reduce) {
  .spotlight.pop:hover::before,
  .spotlight.pop:focus-within::before {
    animation: none;
  }
}

The glow layer is a second radial-gradient placed before the dimming gradient, which creates a crisp inner ring that reads as a subtle highlight. The pop animation animates the CSS variable itself by starting the size at zero and letting the default var value win by the end of the keyframes. The tinted variant adds a hint of brand color near the center without adding any new elements.

Accessibility & Performance

Accessibility

The spotlight effect is decorative, so do not convey unique meaning only through the light. If a card requires status or selection state, keep that in the HTML with text and ARIA. The ::before layer is purely visual, so there is no extra aria markup to add. Mirror the hover effect on focus-within so keyboard navigation gets equivalent feedback. For motion, the prefers-reduced-motion media query above disables size animation while preserving the dimming, which avoids surprise movement for motion-sensitive users.

Performance

Radial gradients repaint on every pointermove. Keep the effect on individual cards instead of a full-page layer. The pseudo-element isolates the repaint region to the card’s box. Avoid stacking multiple large box-shadow animations on the same element; layered radial-gradients are cheaper. If you plan to add cursor tracking to dozens of cards at once, gate the pointermove handler to the hovered element only, which the code already does. Favor short transitions (under 250ms) to keep the UI responsive.

Pack the Spotlight into Your UI Kit

You built a hover spotlight that fades in, dims the surroundings, and can follow the cursor with a small script. The effect is themeable with variables and scales from tiny buttons to hero sections. Now you have a reusable lighting layer you can drop into cards, menus, and feature grids, and you can tune it just as easily as you would make a circle with CSS or any other shape in your design system.

Leave a Comment