Build a Reflective Card in React
Add a realistic light reflection effect to cards that moves with the cursor, making surfaces feel tactile and physical.
Physical objects catch light. Digital cards usually do not. That gap is exactly why a well-implemented reflection effect feels so satisfying when you first encounter it. This one achieves it with SVG filters and CSS gradients, no WebGL required.
The final result
What we are building
A card that tracks the cursor position and uses it to drive a radial gradient background, an SVG turbulence displacement filter for surface texture, a noise texture overlay for grain, and a glossy edge highlight. The result looks like polished metal or glass.
Setting up
npm install lucide-reactimport React, { useRef, useState } from 'react';No animation libraries needed. This is pure CSS and SVG filters.
Building the component
Tracking mouse position
We store normalized coordinates (0 to 1) relative to the card bounds:
function handleMouseMove(e: React.MouseEvent) {
if (!cardRef.current) return;
const rect = cardRef.current.getBoundingClientRect();
setMousePos({
x: (e.clientX - rect.left) / rect.width,
y: (e.clientY - rect.top) / rect.height,
});
}The background reacts to cursor position
The radial gradient origin follows the cursor, creating the impression of a light source moving over the surface:
<div
className='absolute inset-0 z-0'
style={{
background: `radial-gradient(circle at ${mousePos.x * 100}% ${mousePos.y * 100}%,
#2a1a4e 0%, #0d0d2b 40%, #1a0a2e 70%, #0a0a1a 100%)`,
filter: `blur(${blurStrength}px) url(#metallic-displacement)`,
transition: 'background 0.3s ease',
transform: 'scale(1.2)',
}}
/>The scale(1.2) prevents the blurred edges from showing through the card boundary after the filter expands the rendering area.
SVG turbulence creates the metal texture
An SVG filter with feTurbulence, feDisplacementMap, and feSpecularLighting simulates a brushed metal surface:
<filter id='metallic-displacement' x='-20%' y='-20%' width='140%' height='140%'>
<feTurbulence type='turbulence' baseFrequency={baseFrequency} numOctaves='2' result='noise' />
<feColorMatrix in='noise' type='luminanceToAlpha' result='noiseAlpha' />
<feDisplacementMap in='SourceGraphic' in2='noise' scale={displacementStrength}
xChannelSelector='R' yChannelSelector='G' result='rippled' />
<feSpecularLighting in='noiseAlpha' surfaceScale={displacementStrength}
specularConstant={specularConstant} specularExponent='20'
lightingColor='#ffffff' result='light'>
<fePointLight x='0' y='0' z='300' />
</feSpecularLighting>
<feComposite in='light' in2='rippled' operator='in' result='light-effect' />
<feBlend in='light-effect' in2='rippled' mode='screen' result='metallic-result' />
</filter>The baseFrequency is calculated from the noiseScale prop: 0.03 / Math.max(0.1, noiseScale).
Layered overlays
Three additional layers sit on top:
- A noise texture encoded as an SVG data URI adds grain via
mix-blend-mode: overlay - A diagonal gradient at 135 degrees creates the primary sheen
- A 1px border using a gradient mask creates the polished edge highlight
<div className='absolute inset-0 rounded-[20px] p-[1px] bg-[linear-gradient(135deg,rgba(255,255,255,0.8)_0%,rgba(255,255,255,0.2)_50%,rgba(255,255,255,0.6)_100%)] [mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)] [mask-composite:exclude] z-20 pointer-events-none' />How to use it
<ReflectiveCard
blurStrength={12}
metalness={1}
roughness={0.4}
displacementStrength={20}
noiseScale={1}
specularConstant={1.2}
overlayColor="rgba(255, 255, 255, 0.1)"
/>Key takeaways
- The SVG filter is invisible in the DOM (
opacity-0,w-0 h-0) but referenced byurl(#metallic-displacement)in the inline style, which is how you apply SVG filters to HTML elements. - All the visual parameters are exposed as props with sensible defaults, so you can tune the surface from matte to mirror without touching the implementation.
- The noise texture is inlined as a data URI, which means zero extra network requests.