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.
import React, { useRef } from 'react';The component accepts these props:
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:
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:
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:
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:
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
<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-gradientthat slides viabackgroundPosition, not a moving element. This keeps DOM manipulation to a minimum. - Setting
transition: nonebefore repositioning to the start point prevents a visible snap from ruining the re-entry animation. - The
playOnceflag lets you snap the overlay back to start instantly on leave, useful for one-shot reveal cards rather than repeating hovers.