Dirck Mulder
Animations||3 min read

Create a Glare Hover Effect in React

A specular glare follows the cursor across a card surface, giving flat UI elements a physical, reflective quality.

Physical objects reflect light. Flat UI doesn't, and that gap is part of why so much of it feels lifeless. Add a glare that moves with the cursor and your card starts to behave like a real surface, catching and bouncing light as the user moves around.

The final result

What we are building

A diagonal streak of light sweeps across the component on mouse enter and retreats on mouse leave. The animation uses a gradient that slides from outside the visible area through the element and back out. No tilt, no 3D transform. Just a well-timed gradient sweep.

Setting up

You need React with useRef. No extra dependencies.

tsx
import React, { useRef } from 'react';

The component accepts these props:

tsx
interface GlareHoverProps {
  glareColor?: string;
  glareOpacity?: number;
  glareAngle?: number;
  glareSize?: number;
  transitionDuration?: number;
  playOnce?: boolean;
}

Building the component

The first step is converting the hex color to an rgba value so we can apply the opacity:

tsx
const hex = glareColor.replace('#', '');
let rgba = glareColor;
if (/^[\dA-Fa-f]{6}$/.test(hex)) {
  const r = parseInt(hex.slice(0, 2), 16);
  const g = parseInt(hex.slice(2, 4), 16);
  const b = parseInt(hex.slice(4, 6), 16);
  rgba = `rgba(${r}, ${g}, ${b}, ${glareOpacity})`;
}

The overlay div holds the gradient and slides around via backgroundPosition. It starts off-canvas at -100% -100% and animates to 100% 100% on hover:

tsx
const overlayStyle: React.CSSProperties = {
  position: 'absolute',
  inset: 0,
  background: `linear-gradient(${glareAngle}deg,
      hsla(0,0%,0%,0) 60%,
      ${rgba} 70%,
      hsla(0,0%,0%,0) 100%)`,
  backgroundSize: `${glareSize}% ${glareSize}%, 100% 100%`,
  backgroundRepeat: 'no-repeat',
  backgroundPosition: '-100% -100%, 0 0',
  pointerEvents: 'none',
};

The animation functions toggle CSS transitions directly on the element ref:

tsx
const animateIn = () => {
  const el = overlayRef.current;
  if (!el) return;
  el.style.transition = 'none';
  el.style.backgroundPosition = '-100% -100%, 0 0';
  el.style.transition = `${transitionDuration}ms ease`;
  el.style.backgroundPosition = '100% 100%, 0 0';
};

const animateOut = () => {
  const el = overlayRef.current;
  if (!el) return;
  if (playOnce) {
    el.style.transition = 'none';
    el.style.backgroundPosition = '-100% -100%, 0 0';
  } else {
    el.style.transition = `${transitionDuration}ms ease`;
    el.style.backgroundPosition = '-100% -100%, 0 0';
  }
};

The container wires up the mouse events and renders the overlay on top of the children:

tsx
return (
  <div
    className={`relative grid place-items-center overflow-hidden border cursor-pointer ${className}`}
    style={{ width, height, background, borderRadius, borderColor, ...style }}
    onMouseEnter={animateIn}
    onMouseLeave={animateOut}
  >
    <div ref={overlayRef} style={overlayStyle} />
    {children}
  </div>
);

How to use it

tsx
<GlareHover
  width="400px"
  height="300px"
  background="#111"
  borderRadius="12px"
  glareColor="#ffffff"
  glareOpacity={0.4}
  glareAngle={-45}
>
  <p>Your content here</p>
</GlareHover>

Key takeaways

  • The glare is a linear-gradient that slides via backgroundPosition, not a moving element. This keeps DOM manipulation to a minimum.
  • Setting transition: none before repositioning to the start point prevents a visible snap from ruining the re-entry animation.
  • The playOnce flag lets you snap the overlay back to start instantly on leave, useful for one-shot reveal cards rather than repeating hovers.