Glassmorphism Card Hover: Blur Depth Change on Mouse Enter
Learn how to build glassmorphism card hover effects with blur depth changes on mouse enter — pure CSS and React approaches with real Tailwind v4 code examples.
Why Blur Depth on Hover Hits Different
Honestly, most glassmorphism implementations are halfway done. A static backdrop-filter: blur(10px) card that never reacts to the user feels like a screenshot, not a UI. The whole point of the glass metaphor is that depth should feel dynamic — like you're actually moving closer to a frosted pane.
Blur depth change on hover is the trick that sells the illusion. When a card goes from blur(8px) to blur(20px) as your cursor enters, it reads as the glass thickening — the card coming forward to meet you. That's not a gimmick. It's a spatial cue your users read without thinking.
We're going to build this properly: a CSS-first approach, a React hook version, and a Tailwind v4.0.2 utility class setup. No libraries needed. Just a solid understanding of backdrop-filter, transition, and when to reach for CSS variables instead of hardcoded values.
How backdrop-filter: blur() Actually Works
backdrop-filter applies a filter to everything rendered *behind* an element. The key word is behind. The element itself has to be at least partially transparent — otherwise you'll never see the blur effect because the element's own background covers it. This trips up a lot of developers the first time.
The blur() value takes a CSS length, typically in pixels. blur(0px) means no blur. blur(4px) gives a subtle frost. blur(20px) produces the heavy frosted glass look. Values above 30px tend to look muddy on most backgrounds unless the background content is very high-contrast.
Browser support is excellent now. Chrome, Firefox, Safari — all handle it fine. The only edge case worth knowing: Firefox had a pref behind a flag until v103. If you're targeting Firefox users on older versions, add -webkit-backdrop-filter as a fallback prefix. For anything modern, don't worry about it.
One performance note: backdrop-filter triggers compositing on the GPU. It's generally cheap, but stacking many blurred elements on the same page — say, 20+ cards — can cause frame drops on lower-end hardware. Keep that in mind if you're building a grid. If you're curious how other background effects handle GPU compositing, check out the particles background for React breakdown.
The Pure CSS Approach — Transition on Hover
The simplest implementation needs zero JavaScript. You define two states: the default backdrop-filter and the hovered one. CSS transition handles everything between them.
Here's a working card with blur depth change:
.glass-card {
background: rgba(255, 255, 255, 0.10);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 16px;
padding: 24px;
transition:
backdrop-filter 0.35s ease,
-webkit-backdrop-filter 0.35s ease,
background 0.35s ease,
box-shadow 0.35s ease;
}
.glass-card:hover {
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
background: rgba(255, 255, 255, 0.18);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.25),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
}A few things worth noticing. The background opacity goes from 0.10 to 0.18 on hover — this reinforces the blur change by making the card feel slightly more opaque as it comes forward. The inset box-shadow adds a top highlight that mimics reflected light, which sells the glass feel. The 0.35s transition timing is intentional: fast enough to feel snappy, slow enough to register the blur shift.
Tailwind v4.0.2 Implementation with CSS Variables
If you're on Tailwind v4.0.2, the backdrop-blur-* utilities map directly to backdrop-filter: blur(). But to handle the hover state transition, you'll want a CSS variable so you can interpolate cleanly rather than fighting specificity.
// GlassCard.tsx
export function GlassCard({ children }: { children: React.ReactNode }) {
return (
<div
className={[
// base glass
'relative rounded-2xl p-6',
'bg-white/10 border border-white/20',
'backdrop-blur-md', // blur(12px)
// hover state
'hover:bg-white/[0.18] hover:backdrop-blur-xl', // blur(24px)
// transition
'transition-all duration-300 ease-out',
// shadow
'shadow-lg hover:shadow-2xl',
].join(' ')}
>
{children}
</div>
);
}The backdrop-blur-md class maps to blur(12px) and backdrop-blur-xl maps to blur(24px) in Tailwind's default scale. The transition-all duration-300 handles the interpolation. The bg-white/[0.18] uses Tailwind's arbitrary value syntax to hit exactly rgba(255,255,255,0.18) — same target as the CSS version above.
One quirk: transition-all in Tailwind v4 includes backdrop-filter in its transition property set. In v3 it didn't always. If you're on an older version and the blur transition isn't animating, switch to a custom transition utility that explicitly includes backdrop-filter.
Adding a Blur Depth Scale with CSS Custom Properties
What if you want multiple blur levels — not just off/on, but a tiered system? Maybe cards have a default depth, a hover depth, and an active/focused depth. CSS custom properties let you define the scale once and reuse it.
:root {
--blur-sm: 4px;
--blur-md: 10px;
--blur-lg: 20px;
--blur-xl: 32px;
--glass-bg-base: rgba(255, 255, 255, 0.08);
--glass-bg-hover: rgba(255, 255, 255, 0.16);
--glass-bg-active: rgba(255, 255, 255, 0.24);
}
.glass-card {
background: var(--glass-bg-base);
backdrop-filter: blur(var(--blur-sm));
transition: backdrop-filter 0.3s ease, background 0.3s ease;
}
.glass-card:hover {
background: var(--glass-bg-hover);
backdrop-filter: blur(var(--blur-md));
}
.glass-card:active {
background: var(--glass-bg-active);
backdrop-filter: blur(var(--blur-lg));
}This approach makes theming trivial. Want a dark-mode glassmorphism variant? Override the --glass-bg-* variables under a [data-theme='dark'] selector. No component changes needed. If you want to understand glassmorphism fundamentals before building on top of them, the what is glassmorphism article covers the design principles in depth.
The --blur-xl at 32px is there for special cases — a modal overlay, a sidebar that slides over content. Don't use it on regular cards. It's too heavy and kills legibility of the background content.
React Hook Version for Programmatic Blur Control
Sometimes you need blur control that CSS alone can't provide. Blur that responds to cursor *position within* the card (not just enter/leave), or blur that syncs with an animation state from elsewhere in the app. That's where a React hook comes in.
import { useRef, useState, useCallback } from 'react';
function useBlurOnHover(minBlur = 8, maxBlur = 22) {
const [blur, setBlur] = useState(minBlur);
const ref = useRef<HTMLDivElement>(null);
const handleMouseMove = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!ref.current) return;
const rect = ref.current.getBoundingClientRect();
// Distance from card center (0 = center, 1 = edge)
const dx = (e.clientX - (rect.left + rect.width / 2)) / (rect.width / 2);
const dy = (e.clientY - (rect.top + rect.height / 2)) / (rect.height / 2);
const dist = Math.min(Math.sqrt(dx * dx + dy * dy), 1);
// Blur increases as cursor moves toward edges
const computed = minBlur + (maxBlur - minBlur) * dist;
setBlur(Math.round(computed * 10) / 10);
},
[minBlur, maxBlur]
);
const handleMouseLeave = useCallback(() => {
setBlur(minBlur);
}, [minBlur]);
return { ref, blur, handleMouseMove, handleMouseLeave };
}
// Usage
export function DynamicGlassCard() {
const { ref, blur, handleMouseMove, handleMouseLeave } = useBlurOnHover(6, 20);
return (
<div
ref={ref}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{
backdropFilter: `blur(${blur}px)`,
WebkitBackdropFilter: `blur(${blur}px)`,
background: 'rgba(255, 255, 255, 0.12)',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: '16px',
padding: '24px',
transition: 'backdrop-filter 0.1s ease',
}}
>
<p>Blur: {blur}px</p>
</div>
);
}This hook maps cursor distance from the card's center to a blur value between 6px and 20px. Moving the cursor to the edges increases the blur; centering it decreases it. The 0.1s transition smooths out the per-frame updates without introducing lag. Pairing this with an aurora background behind the card makes the depth shift really pop.
Common Mistakes and How to Fix Them
The blur isn't animating? Nine times out of ten it's because you didn't include backdrop-filter in your transition property. Writing transition: all 0.3s should catch it, but if you're using shorthand transitions that exclude filter properties, it'll snap instead of interpolate.
The card looks washed out? Your background opacity is probably too high. rgba(255,255,255,0.15) is a solid starting point for light glass on a colorful background. Go higher than 0.25 and you lose the frosted effect — it just looks like a semi-transparent white box.
Safari showing a flicker on hover? This is the classic GPU layer promotion bug. Add will-change: backdrop-filter to pre-promote the element to its own compositor layer. That said, will-change isn't free — only apply it to elements you know will animate. Don't scatter it everywhere.
Want to theme your glass cards to match dark/light mode? The theme toggle in React article walks through the CSS variable approach that pairs perfectly with the custom property system described earlier in this article.
When to Use Blur Depth Change (And When Not To)
Blur depth hover effects work best on cards that sit over rich, high-contrast backgrounds — gradients, images, animated scenes. On a flat white or flat dark background, the blur effect has nothing to blur. You'll get the hover state CSS firing but no visible change. Is your background actually giving the filter something to work with? That's the question to ask first.
Avoid blur depth hover on very small elements — anything under roughly 80px × 80px. The effect is too subtle to read at that scale. It's also worth skipping it on elements that are already in motion, like items in a carousel or infinite scroll list. The combined GPU load of constant compositing plus blur transition can tank performance on mobile.
For cards in a dashboard or SaaS app, this effect is genuinely useful as a focus indicator — it replaces the ugly browser default outline with something that fits the aesthetic. For ecommerce product cards, it can work but keep the blur delta small (say, blur(6px) to blur(12px)) so it doesn't distract from the product image. Context matters.
FAQ
Yes, in modern browsers. Chrome, Edge, Firefox 103+, and Safari all interpolate backdrop-filter values when you include it in a CSS transition or animation. The one edge case is older Firefox versions (pre-103) where the property was behind a flag — those browsers will snap between states rather than animate.
Two common causes. First, the element's background is fully opaque — backdrop-filter only affects what shows through, so if your background-color is solid (e.g., background: white), there's nothing to blur. Set it to a semi-transparent value like rgba(255,255,255,0.12). Second, there's nothing visually interesting behind the card. On a flat background, blur produces no visible result.
On a single card, no. The GPU handles it fine. With 20+ blurred cards on screen simultaneously — especially if they're all transitioning at once — you might see frame drops on mid-range mobile devices. Use will-change: backdrop-filter sparingly on cards you know will animate, and test on real hardware, not just DevTools throttling.
Use box-shadow for the glow — it composites on the GPU alongside backdrop-filter without adding extra DOM elements. A value like 0 0 24px rgba(99, 102, 241, 0.5) gives a purple glow. Include box-shadow in your transition property alongside backdrop-filter so both animate together.
In Tailwind v4.0.2, yes. The backdrop-blur-{size} utilities cover most common blur values (sm=4px, md=12px, lg=16px, xl=24px, 2xl=40px). Pair with hover:backdrop-blur-xl and transition-all duration-300 and you get a working blur depth hover with no custom CSS. For non-standard values, use the arbitrary value syntax: backdrop-blur-[18px].
Pure CSS handles it fine for enter/leave transitions. You only need JavaScript if you want cursor-position-aware blur (blur changing based on where in the card the mouse is), or if you need to sync the blur with external app state. For the standard hover-in/hover-out pattern, the :hover pseudo-class plus transition covers everything.