The current look of this site — a near-black background with soft glowing orbs drifting behind frosted-glass cards — is built entirely with CSS. No background images, no canvas, no JavaScript animation loop. This post breaks down the three techniques behind it: a token system with custom properties, animated aurora orbs, and glassmorphism with backdrop-filter.
Step One: A Token System
Before any visuals, I define the entire palette and scale as CSS custom properties on :root. This is the backbone — every component references tokens, never raw values:
:root {
--bg: #050507;
--bg-surface: rgba(255, 255, 255, 0.03);
--bg-elevated: rgba(255, 255, 255, 0.05);
--text: #f4f4f5;
--text-secondary:#a1a1aa;
--text-muted: #52525b;
--border: rgba(255, 255, 255, 0.08);
--accent: #6366f1; /* electric indigo */
--accent-glow: #818cf8;
--accent-subtle: rgba(99, 102, 241, 0.12);
--radius: 14px;
--radius-lg: 20px;
--transition: 0.25s ease;
}
The payoff is huge: because --accent is a single variable, I can re-theme the entire site by changing one value — and even let JavaScript overwrite it at runtime for a user-selectable accent color.
Step Two: The Aurora Background
The glowing orbs are just large, heavily-blurred radial gradients positioned behind everything and slowly animated. Each "orb" is a pseudo-element:
body::before,
body::after {
content: '';
position: fixed;
border-radius: 50%;
filter: blur(100px);
opacity: 0.5;
z-index: -1;
pointer-events: none;
}
body::before { /* indigo orb, top-left */
width: 45vw; height: 45vw;
top: -10%; left: -10%;
background: radial-gradient(circle,
rgba(99,102,241,0.5), transparent 70%);
animation: drift 18s ease-in-out infinite;
}
body::after { /* cyan orb, bottom-right */
width: 40vw; height: 40vw;
bottom: -15%; right: -10%;
background: radial-gradient(circle,
rgba(34,211,238,0.4), transparent 70%);
animation: drift 22s ease-in-out infinite reverse;
}
The motion comes from one reusable keyframe that nudges position and scale:
@keyframes drift {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(6%, 8%) scale(1.15); }
}
The heavy blur(100px) is what turns two hard gradient circles into a soft, atmospheric glow. Two pseudo-elements, zero images, and the background feels alive.
Step Three: Glassmorphism Cards
Cards sit on top of the aurora as frosted glass. The recipe is a translucent background plus backdrop-filter: blur(), which blurs whatever is behind the element:
.glass-card {
background: var(--bg-surface);
backdrop-filter: blur(12px) saturate(140%);
-webkit-backdrop-filter: blur(12px) saturate(140%);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
Three details sell the glass effect:
- The translucent background lets the aurora bleed through faintly.
- The 1px hairline border in a low-opacity white gives the edge a "lit rim," the way real glass catches light.
- saturate(140%) on the backdrop boosts the color behind the card so the glow reads through richer.
Performance: The Catch
backdrop-filter and large blurs are GPU-intensive. On weaker devices, dozens of glass cards plus animated orbs can drop the frame rate. My mitigations:
- A performance toggle. A "reduce effects" setting swaps
backdrop-filterfor a solid surface color and pauses the orb animation. - Respect
prefers-reduced-motion. Users who ask for less motion get the orbs frozen automatically:
@media (prefers-reduced-motion: reduce) {
body::before, body::after { animation: none; }
}
- Limit the blurred layers. Two orbs and glass only on real cards — not every box — keeps the compositor happy.
Accessibility and Contrast
Dark, glowing designs can quietly fail contrast checks. Translucent text over a busy background is the usual culprit. I keep body text at a solid --text-secondary rather than a low alpha, ensure headings hit the 4.5:1 ratio against the darkest background, and never put essential text directly over the brightest part of an orb.
Why CSS-Only Was the Right Call
I could have used a canvas particle system or a WebGL shader for the background. CSS won because it's lightweight (no JS bundle, no render loop competing with the main thread), it degrades gracefully (older browsers just see a dark background), and it themes instantly through custom properties. For an atmospheric portfolio background, a couple of blurred gradients and a keyframe deliver 90% of the effect at a tiny fraction of the cost.
Takeaways
Build on a token system first — it makes everything after it themeable. Reach for pseudo-elements and filter: blur() before images or canvas. And always pair flashy effects with an escape hatch for performance and accessibility. The whole aesthetic of this site rests on those three moves, and you can read every line of it in the stylesheet.