Aurora Card Component in React: Shifting Gradient Glass Effect
Build a React aurora card with animated shifting gradients and glass blur — complete code, Tailwind config, and performance tips for 2026 browsers.
What the Aurora Card Effect Actually Is
The aurora card isn't just a gradient card with a blur layer slapped on top. It's a specific combination: animated conic or mesh gradients that shift over time, a backdrop-filter: blur() glass surface sitting above them, and subtle radial light blobs that give the impression of northern-lights movement behind frosted glass. The result looks like you're holding a piece of sea glass over the sky at 2am in Tromsø.
Practically speaking, you're combining two visual ideas that originated separately. Glassmorphism — popularized after macOS Big Sur shipped in late 2020 — gave us the frosted layer on top. The aurora gradient underneath borrows from the mesh gradient trend that took off around 2023–2024, where multiple colored radial gradients overlap and animate. Stack them and you get something that reads as neither of those things alone.
Worth noting: the aurora style hub on Empire UI treats this as its own design language, not just a glassmorphism variant. There's good reason for that — the animation layer changes the character of the component entirely. A static glass card feels modern. An aurora card feels alive. That's not an aesthetic opinion, it's a functional difference in how users perceive interactivity.
If you want to understand what makes glass-over-gradient work before building it from scratch, spend five minutes with the glassmorphism generator tuning blur and transparency values. You'll immediately see why 12px blur with 0.12 background alpha hits different from 4px at 0.3.
Setting Up the Gradient Background Layer
Everything sits on top of a dark base — usually #050510 or similar near-black. The aurora effect looks flat on white backgrounds. Dark surfaces let the gradient colors punch through the glass without washing out.
The moving gradient layer is built from three to four absolutely-positioned divs, each carrying a single radial gradient blob. You animate their transform: translate() values on a slow loop — 8 to 12 seconds works well. Fast animations look frantic; you want drift, not flash.
// AuroraBackground.tsx
const blobs = [
{ color: 'rgba(99, 102, 241, 0.6)', size: 600, x: 10, y: 20 },
{ color: 'rgba(168, 85, 247, 0.5)', size: 500, x: 60, y: 10 },
{ color: 'rgba(34, 211, 238, 0.45)', size: 550, x: 30, y: 60 },
{ color: 'rgba(16, 185, 129, 0.4)', size: 450, x: 70, y: 70 },
];
export function AuroraBackground({ children }: { children: React.ReactNode }) {
return (
<div className="relative overflow-hidden bg-[#050510] min-h-screen">
{blobs.map((blob, i) => (
<div
key={i}
className="absolute rounded-full blur-[80px] aurora-blob"
style={{
width: blob.size,
height: blob.size,
left: `${blob.x}%`,
top: `${blob.y}%`,
background: blob.color,
animationDelay: `${i * 1.5}s`,
}}
/>
))}
<div className="relative z-10">{children}</div>
</div>
);
}The aurora-blob class does the actual animation. Drop this in your globals.css or Tailwind's @layer utilities:
``css
@keyframes aurora-drift {
0%, 100% { transform: translate(0px, 0px) scale(1); }
33% { transform: translate(30px, -20px) scale(1.05); }
66% { transform: translate(-20px, 15px) scale(0.97); }
}
.aurora-blob {
animation: aurora-drift 10s ease-in-out infinite;
}
``
One more thing — the blur-[80px] Tailwind class requires you to have arbitrary value support enabled, which is on by default in Tailwind v3.3+. If you're still on v3.1 or earlier, write it as filter: blur(80px) inline. Honestly, 80px is the sweet spot here — anything under 60px and the blobs look like colored smudges rather than ambient light.
Building the Glass Card Component
The card itself is straightforward CSS — the magic is in the layering. You need backdrop-filter: blur(12px) on the card, a semi-transparent background (rgba(255,255,255,0.06) on dark is a good start), and a border that catches the light. The border is where most tutorials cut corners.
A single border: 1px solid rgba(255,255,255,0.12) works fine. But if you want the card to look like it has an actual glass edge, use a subtle gradient border instead:
``tsx
// AuroraCard.tsx
import { ReactNode } from 'react';
interface AuroraCardProps {
children: ReactNode;
className?: string;
}
export function AuroraCard({ children, className = '' }: AuroraCardProps) {
return (
<div
className={relative rounded-2xl p-[1px] ${className}}
style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0.03) 50%, rgba(255,255,255,0.08) 100%)',
}}
>
<div
className="rounded-2xl p-6"
style={{
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)',
background: 'rgba(10, 10, 20, 0.45)',
}}
>
{children}
</div>
</div>
);
}
``
The outer div is a 1px wrapper with a gradient background — this creates the gradient border without any CSS border-image hacks that break border-radius. It's a trick borrowed from the glassmorphism components pattern and it's become standard practice in 2026 because it actually works consistently across browsers.
In practice, you'll want to tune the inner background opacity based on how saturated your aurora blobs are. High-intensity purple/cyan blobs need a darker inner background (around 0.5 alpha) or the card becomes unreadable. More pastel blobs can drop to 0.3 and still look fine. There's no universal answer — test with your actual content colors.
Quick aside: always include -webkit-backdrop-filter alongside backdrop-filter. Safari still requires the prefix as of Safari 17.x even though it's technically a standard property now. Skipping it means the glass effect breaks for iPhone users, and that's never a good look.
Adding a Mouse-Tracking Highlight
The static version looks good. The interactive version looks great. A mouse-tracking radial gradient that follows the cursor inside the card — sometimes called a "spotlight" or "shine" effect — makes the glass feel physically reactive. It's the difference between a nice UI element and one that people screenshot.
import { useRef, useState, MouseEvent } from 'react';
export function AuroraCardInteractive({ children }: { children: React.ReactNode }) {
const cardRef = useRef<HTMLDivElement>(null);
const [spotlight, setSpotlight] = useState({ x: 50, y: 50, opacity: 0 });
function handleMouseMove(e: MouseEvent<HTMLDivElement>) {
const rect = cardRef.current?.getBoundingClientRect();
if (!rect) return;
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
setSpotlight({ x, y, opacity: 1 });
}
function handleMouseLeave() {
setSpotlight(prev => ({ ...prev, opacity: 0 }));
}
return (
<div
ref={cardRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
className="relative rounded-2xl p-6 overflow-hidden"
style={{
backdropFilter: 'blur(12px)',
background: 'rgba(10, 10, 20, 0.45)',
}}
>
<div
className="pointer-events-none absolute inset-0 rounded-2xl transition-opacity duration-300"
style={{
opacity: spotlight.opacity,
background: `radial-gradient(200px circle at ${spotlight.x}% ${spotlight.y}%, rgba(255,255,255,0.08) 0%, transparent 70%)`,
}}
/>
<div className="relative z-10">{children}</div>
</div>
);
}The spotlight gradient sits as an absolutely-positioned overlay inside the card. pointer-events-none keeps it from blocking clicks. The transition-opacity duration-300 on the wrapper handles the fade-in/out when the mouse enters or leaves — no JavaScript animation loop needed.
Why 200px circle radius? It's the sweet spot for a 320–400px wide card. Smaller and it looks like a flashlight beam. Larger and it floods the whole card and loses the focus effect. If you're building cards wider than 500px — say for a dashboard panel — bump it to 300–350px instead.
Look, the mouse tracking adds about 15 lines of code and zero runtime cost since it's just CSS gradient recalculation. There's no reason not to include it. The getBoundingClientRect() call on every mousemove event is fine — browsers batch and schedule these efficiently, and you're not doing any heavy computation in the handler.
Composing the Full Component
Now you've got three pieces: the background layer, the glass card shell, and the spotlight interaction. Here's how they compose into something you'd actually ship:
``tsx
// Usage in a page or layout
import { AuroraBackground } from './AuroraBackground';
import { AuroraCardInteractive } from './AuroraCardInteractive';
export default function FeaturePage() {
return (
<AuroraBackground>
<div className="flex min-h-screen items-center justify-center p-8">
<AuroraCardInteractive>
<div className="max-w-sm">
<p className="mb-1 text-xs font-semibold uppercase tracking-widest text-indigo-400">
New Feature
</p>
<h2 className="mb-3 text-2xl font-bold text-white">
Aurora Dashboard
</h2>
<p className="mb-6 text-sm leading-relaxed text-white/60">
Real-time analytics with an interface that actually looks like
it belongs in 2026.
</p>
<button className="rounded-lg bg-indigo-500/80 px-4 py-2 text-sm font-medium text-white backdrop-blur-sm hover:bg-indigo-500 transition-colors">
Get started
</button>
</div>
</AuroraCardInteractive>
</div>
</AuroraBackground>
);
}
``
A few composition notes. The text inside should use text-white or text-white/80 — never grey text on the aurora background. The aurora blobs already reduce contrast in unpredictable ways; grey text on shifting gradients is a readability disaster. Stick to white with opacity variants for hierarchy.
The button inside uses backdrop-blur-sm itself — a nested glass effect. It sounds excessive but it actually grounds the button visually inside the card. Without it, solid buttons look pasted on. With it, everything reads as part of the same material.
That said, don't go overboard nesting blur contexts. Each backdrop-filter creates a new stacking context and composite layer in the browser. Two levels (background → card → button) is the practical limit before you start seeing Safari repaint jank on scroll. Check your Layers panel in Chrome DevTools if you're seeing unexpected compositing cost.
For more Aurora-style components and ready-to-use examples, the aurora style hub has patterns you can pull directly — including cards, hero sections, and nav bars all built on the same blob-plus-glass foundation.
Performance Considerations You Can't Skip
backdrop-filter is GPU-accelerated in all modern browsers, but it's not free. Each blurred element forces the browser to composite that layer separately, which adds GPU memory pressure. On a simple marketing page with one card, you won't notice. On a dashboard with 12 aurora cards visible at once, you will.
The blob animations are the other cost center. Animating transform keeps them on the compositor thread — good. But if you animate left/top positions instead (a common mistake in tutorials), you trigger layout recalculation on every frame and you'll see 16ms+ frame times on mid-range hardware. Always use transform: translate() for blob movement.
/* BAD — triggers layout on every frame */
@keyframes bad-drift {
0% { left: 10%; top: 20%; }
50% { left: 15%; top: 25%; }
}
/* GOOD — compositor thread, no layout cost */
@keyframes good-drift {
0% { transform: translate(0px, 0px); }
50% { transform: translate(5%, 5%); }
}One more thing — add will-change: transform to your blob elements if you're targeting lower-end devices. It tells the browser to promote those elements to their own layer upfront rather than promoting mid-animation, which removes a jank spike on first render. Don't spray will-change everywhere though; each promoted layer costs GPU memory.
Honestly, the aurora effect is heavier than a flat card component. That's the trade-off. If you're building for low-power devices or battery-sensitive mobile users, consider a prefers-reduced-motion media query that disables the blob animations and falls back to a static gradient. The glass effect itself can stay — it's the continuous animation that drains battery.
Accessibility and Dark Mode Compatibility
Aurora cards are inherently dark-surface components. They don't translate well to light mode — the frosted glass over aurora blobs needs that near-black background to read correctly. If your app has a light mode, the pragmatic call is to skip the aurora effect entirely in light mode and use a different card style from the Empire UI component library.
Contrast is the accessibility concern that actually bites people. WCAG 2.1 AA requires 4.5:1 contrast ratio for body text and 3:1 for large text. White text (#ffffff) on rgba(10,10,20,0.45) backed by a dark purple blob passes — but only if the blob doesn't drift directly behind your text at a moment where the colors wash out. Test at multiple animation frames, not just the initial state.
// Respect prefers-reduced-motion
import { useEffect, useState } from 'react';
function useReducedMotion() {
const [reduced, setReduced] = useState(false);
useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
setReduced(mq.matches);
const handler = (e: MediaQueryListEvent) => setReduced(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return reduced;
}
// In AuroraBackground, pass this to the blob className:
// reduced ? '' : 'aurora-blob'Focus states on interactive cards need explicit handling too. The default browser focus ring tends to get lost against the dark glass surface. Use a focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent Tailwind chain on any focusable elements inside the card. It keeps keyboard navigation visible without looking out of place.
What's the point of building a stunning aurora card if half your users can't read the content or navigate it with a keyboard? The visual effect is worth exactly nothing if it creates barriers. Get the contrast right, test with a screen reader, add the reduced-motion fallback — then ship it.
FAQ
Yes — Chromium, Firefox, and Safari all support it as of 2024. Always include -webkit-backdrop-filter alongside it for Safari compatibility, even on modern versions.
Not really. The effect depends on a dark background to make the gradient blobs visible through the glass. On white or light grey, the frosted layer just looks muddy.
Use transform: translate() for movement, never left/top or background-position. Transform-based animations run on the compositor thread and don't trigger layout recalculation.
Similar technique, different character. Glassmorphism cards are typically static — the aurora variant adds animated gradient blobs underneath that shift over time, which changes the feel from "modern" to "alive".