Glassmorphism Blog Layout: Frosted Article Cards and Reading View
Build a frosted-glass blog layout in React and Next.js — article grid cards, single-post reading view, and the backdrop-filter tricks that make it feel real.
Why a Frosted Blog Layout Actually Works
Blog layouts are boring by default. You get a white card, a thumbnail, a title, a date — done. The glassmorphism treatment doesn't just make that look better. It changes the entire reading experience by giving depth to what's otherwise a flat grid.
The core idea is simple: semi-transparent surfaces layered over a blurred, colorful background. When you scroll through article cards and each one feels like frosted glass sitting above a gradient world, there's a tactile quality that pulls people in. It's not a gimmick if it serves the reading flow.
Honestly, the technique peaked hard around 2021 but the 2026 version is much more restrained — lighter blur radii, less white opacity, and backgrounds that don't scream gradient pack. If you look at where glassmorphism components are heading, the trend is toward subtle depth rather than full frosted overload. One card that breathes is worth ten that suffocate.
This article walks through both layers of the problem: the article grid (the index page), and the single post reading view. They need different treatments — cards compress context, reading views expand it — but they share the same CSS foundation.
Setting Up the CSS Foundation: backdrop-filter and Beyond
Everything in a glassmorphism layout hinges on backdrop-filter: blur(). No blur, no glass. Before you write a single component, make sure your background layer exists — without something colorful behind the frosted surface, the blur has nothing to work with and you just get gray.
Here's the minimal CSS system you need. These custom properties wire up consistently across cards and reading containers:
``css
:root {
--glass-bg: rgba(255, 255, 255, 0.08);
--glass-border: rgba(255, 255, 255, 0.18);
--glass-blur: 14px;
--glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
--glass-radius: 16px;
}
.glass {
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur)); /* Safari */
border: 1px solid var(--glass-border);
border-radius: var(--glass-radius);
box-shadow: var(--glass-shadow);
}
``
Worth noting: Safari still needs -webkit-backdrop-filter as of 2026, even though Chrome, Firefox, and Edge handle the unprefixed version fine. Don't skip it or half your mobile traffic looks broken. Also — backdrop-filter requires the element to have a stacking context, which means you'll need position: relative or a will-change hint somewhere in the chain.
The background layer that sits behind everything should be a gradient or mesh. A fixed-position pseudo-element on body works well — it stays put when you scroll, so cards reveal different color zones as the page moves. You can generate one that fits your palette with the gradient generator rather than eyeballing HSL values.
One more thing — backdrop-filter is GPU-accelerated but it isn't free. Stack more than 4–5 blurred layers on the same viewport and you'll feel it on older hardware. For a blog card grid that renders 12–20 cards, keep your blur radius at or below 16px and test on real devices.
Building the Article Card Component
The card is the unit everything else is built from. It needs to display a thumbnail, category badge, title, excerpt snippet, author, and date — all inside a frosted surface that doesn't feel cluttered. That's a lot to fit in roughly 320–400px of width.
Here's the React component using Tailwind. The trick is a subtle inner highlight on the top border that simulates light hitting frosted glass at an angle:
``tsx
interface ArticleCardProps {
slug: string;
title: string;
excerpt: string;
category: string;
date: string;
readMin: number;
image: string;
imageAlt: string;
}
export function ArticleCard({
slug, title, excerpt, category, date, readMin, image, imageAlt
}: ArticleCardProps) {
return (
<a
href={/blog/${slug}}
className="group block rounded-2xl overflow-hidden
bg-white/8 backdrop-blur-[14px]
border border-white/18
shadow-[0_8px_32px_rgba(0,0,0,0.25)]
hover:bg-white/12 hover:border-white/28
hover:shadow-[0_12px_40px_rgba(0,0,0,0.35)]
transition-all duration-300"
>
<div className="relative h-48 overflow-hidden">
<img
src={image}
alt={imageAlt}
className="w-full h-full object-cover
group-hover:scale-105 transition-transform duration-500"
/>
<span className="absolute top-3 left-3 px-2 py-1 rounded-full
bg-white/20 backdrop-blur-sm
text-white text-xs font-medium uppercase tracking-wide">
{category}
</span>
</div>
<div className="p-5">
<h2 className="text-white font-semibold text-lg leading-snug
mb-2 line-clamp-2 group-hover:text-white/90">
{title}
</h2>
<p className="text-white/60 text-sm leading-relaxed line-clamp-3 mb-4">
{excerpt}
</p>
<div className="flex items-center justify-between text-white/40 text-xs">
<span>{date}</span>
<span>{readMin} min read</span>
</div>
</div>
</a>
);
}
``
The hover state matters. You're nudging the background opacity from white/8 to white/12 and tightening the shadow — that's enough to communicate interactivity without a jarring jump. Quick aside: line-clamp-2 and line-clamp-3 are now baseline CSS, so you don't need the @tailwindcss/line-clamp plugin if you're on Tailwind v3.3+.
For the category badge sitting on the image, you're applying a second, smaller backdrop-blur-sm inside the card's own blur layer. This double-blur creates the feeling that the badge is glass sitting on glass — it's subtle but it reads as premium. Keep that badge's background at no more than rgba(255,255,255,0.20) or it looks like a sticky label.
In practice, the card thumbnail drives the whole feel. A muted, colorful photo shows off the blur effect beautifully. A white-background product shot kills it instantly. If you're pulling from Unsplash or your own CMS, filter for photos with soft gradients or bokeh — the blur has something to interact with.
Wiring the Blog Index Grid in Next.js
The grid layout for the index page is where the frosted cards live. You want a responsive masonry-style or even-column grid with a fixed background that stays put — so scrolling reveals new color zones through the glass.
Here's a Next.js App Router page with static data fetching and the fixed background approach:
``tsx
// app/blog/page.tsx
import { ArticleCard } from '@/components/ArticleCard';
import { getArticles } from '@/lib/blog';
export default async function BlogIndex() {
const articles = await getArticles();
return (
<>
{/* Fixed gradient background */}
<div
className="fixed inset-0 -z-10"
style={{
background:
radial-gradient(ellipse at 20% 20%, #6366f1 0%, transparent 50%),
radial-gradient(ellipse at 80% 80%, #ec4899 0%, transparent 50%),
radial-gradient(ellipse at 50% 50%, #0ea5e9 0%, transparent 70%),
#0f0f1a
}}
/>
<main className="min-h-screen px-4 py-20">
<div className="max-w-6xl mx-auto">
<header className="mb-12 text-center">
<h1 className="text-5xl font-bold text-white mb-4">
From the Blog
</h1>
<p className="text-white/60 text-lg">
Design, code, and everything in between.
</p>
</header>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{articles.map((article) => (
<ArticleCard key={article.slug} {...article} />
))}
</div>
</div>
</main>
</>
);
}
``
The fixed inset-0 -z-10 pattern is the key move here. The gradient sits in the page's stacking context below everything, doesn't scroll, and gives every frosted card a live, shifting color to blur through as the reader scrolls down. No JavaScript scroll listeners needed.
That said, if you're using a CMS like Contentful or a headlined JSON file, the getArticles() call is just a fetch. In Next.js 15, Server Components fetch at build time by default — you'll get a fully static page with zero client-side JavaScript for the grid itself. Cards are links, not buttons, so that's fine.
One thing to watch: the grid gap at 24px (the gap-6 class) gives each card breathing room so their blur zones don't visually bleed into each other. Drop that to gap-4 and the cards start competing. At gap-8 on a 3-column grid you're wasting real estate. 24px is the sweet spot for this card size.
The Reading View: Single Post Layout
The article reading view is a different design problem. Cards compress — reading views expand. You want more whitespace, better typography, and a frosted container that doesn't fight the prose. The temptation is to go all-in on glass for the reading pane. Resist it.
What actually works: a single frosted column, 680px max-width, centered, with the background gradient continuing behind it. The glass effect here should be subtle — rgba(255,255,255,0.05) and a 12px blur rather than 14px. The lower opacity lets the gradient colors breathe through without distracting from the text.
``tsx
// app/blog/[slug]/page.tsx
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const article = await getArticle(params.slug);
return (
<>
<div
className="fixed inset-0 -z-10"
style={{
background:
radial-gradient(ellipse at 30% 10%, #7c3aed 0%, transparent 55%),
radial-gradient(ellipse at 70% 90%, #be185d 0%, transparent 55%),
#0a0a14
}}
/>
<main className="min-h-screen px-4 py-20">
{/* Reading container */}
<article
className="max-w-[680px] mx-auto
bg-white/5 backdrop-blur-[12px]
border border-white/12
rounded-2xl shadow-[0_16px_48px_rgba(0,0,0,0.4)]
overflow-hidden"
>
{/* Hero image */}
<div className="relative h-64 sm:h-80">
<img
src={article.image}
alt={article.imageAlt}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
</div>
{/* Content */}
<div className="p-8 sm:p-12">
<div className="flex items-center gap-3 mb-6 text-sm text-white/50">
<span>{article.category}</span>
<span>·</span>
<span>{article.date}</span>
<span>·</span>
<span>{article.readMin} min read</span>
</div>
<h1 className="text-3xl sm:text-4xl font-bold text-white leading-tight mb-6">
{article.title}
</h1>
<div
className="prose prose-invert prose-p:text-white/75
prose-headings:text-white prose-a:text-purple-400
prose-code:bg-white/10 prose-code:text-pink-300
max-w-none"
dangerouslySetInnerHTML={{ __html: article.contentHtml }}
/>
</div>
</article>
</main>
</>
);
}
``
The prose-invert class from @tailwindcss/typography handles the base reading styles. You're overriding the defaults to push prose text to white/75 instead of full white — that slight reduction in contrast is easier on the eyes for long reads and actually makes the headings (at full white) pop more.
Look, the hero image gradient overlay (from-black/60 to-transparent) is doing double duty here. It makes the hero image fade into the glass surface below it so there's no hard edge. Without that, you get a rectangle of photo sitting on a frosted rectangle and it looks like a UI mockup, not a finished product.
Quick aside: the reading container should NOT be full-viewport-width on desktop. max-w-[680px] keeps lines around 65–75 characters wide, which is the typographic sweet spot for comfortable reading. You can check more layout patterns like this in the glassmorphism components collection.
Animations and Micro-Interactions
Static glass is fine. Animated glass is better. The two interactions worth adding are card entrance animations on the index page and a smooth scroll-into-view for long articles. Neither needs a heavy library.
For card entrances, a simple CSS animation with staggered animation-delay based on index gives a cascade effect that feels expensive without costing much:
``tsx
// In your grid map:
{articles.map((article, i) => (
<div
key={article.slug}
style={{
animationDelay: ${i * 60}ms,
animation: 'cardEnter 0.5s ease-out both'
}}
>
<ArticleCard {...article} />
</div>
))}
// In globals.css:
@keyframes cardEnter {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
``
20px of vertical movement is plenty. You don't need 60px of dramatic drop — that reads as flashy on first visit and then annoying on every subsequent page load. Keep the duration at 500ms max. If you want something more polished and physics-based, Framer Motion is the go-to, but for a blog index this CSS approach builds zero JavaScript.
For reading view, consider a reading progress indicator — a thin line at the top of the viewport that fills as the user scrolls. It's especially useful on long posts and fits the glass aesthetic beautifully at height: 2px, background: linear-gradient(to right, #a78bfa, #ec4899). The Empire UI glassmorphism generator can help you find matching accent colors for that gradient.
That said, don't overdo it. A frosted background with animated cards and a progress bar and a parallax hero and a sticky navbar all doing their own animated thing becomes overwhelming fast. Pick two micro-interactions maximum and let the frosted glass itself do the visual heavy lifting.
Dark Mode, Accessibility, and Performance
Here's the thing about glassmorphism and accessibility: low contrast is your enemy. white/60 text on a semi-transparent card might look great in Figma and fail WCAG 2.1 AA the second you put it in front of an actual contrast checker. Article text especially — the prose inside the reading container — needs to hit at least 4.5:1 against the card background.
The safest approach is to not rely on the frosted surface alone for contrast. Add a subtle darker overlay beneath the text sections of your cards:
``css
.card-text-region {
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 0.3)
);
}
``
This isn't a hack — it's how film and photo UIs have solved floating text readability for years. The gradient is invisible at the top (where the image is), deepens toward the bottom (where your title text lives), and gives you the contrast you need without making the card look dark and heavy.
Performance-wise, monitor your Cumulative Layout Shift. The fixed-background gradient doesn't cause CLS but the card images absolutely will if you don't set explicit dimensions. In Next.js, the <Image> component handles this automatically with the width and height props — or use fill mode inside a sized container. And add loading="lazy" to all images below the fold. The first 3 cards should load eagerly; everything else can wait.
Worth noting: backdrop-filter triggers compositing layers. On mobile, it can cause janky scroll if you have too many simultaneously visible blurred elements. A common fix is will-change: transform on the card wrapper, which promotes each card to its own layer. Test on a mid-range Android device from 2023 — that's your real performance baseline, not a MacBook Pro.
FAQ
Yes, with the -webkit- prefix for Safari. Chrome, Firefox, Edge, and Safari all support it — just don't skip the prefixed version or Safari users get no blur effect.
Technically yes, but the effect is much weaker. Glassmorphism needs a colorful, dark-ish background to show the blur clearly. On a white page, you mostly just get a slightly gray card.
Between 10px and 16px for most use cases. Go higher and the blur starts eating GPU budget; go lower and it stops reading as glass. 14px is a reliable default.
Keep body text at rgba(255,255,255,0.75) minimum and add a subtle dark gradient behind text regions. Run it through a contrast checker — WCAG AA requires 4.5:1 for normal text.