A product section where every piece comes into focus in turn. The label, heading, paragraph and button appear first, then six pottery cards follow one by one. Each item starts blurred, transparent and a few pixels low, then sharpens and settles as it enters the screen. A Replay button runs the sequence again. The effect is a small wrapper you can put around any element. The grid is two columns on phones and three on desktop.
// Adapted from Magic UI (https://magicui.design/docs/components/blur-fade)
// Copyright (c) Magic UI. MIT License: https://we.inc/components/licenses
import { useRef, useState, type ReactNode } from "react";
import { motion, useInView, useReducedMotion } from "framer-motion";
import { RotateCcw } from "lucide-react";
const pieces = [
{ name: "Dune vase", maker: "Oda Lindqvist", price: "$84", art: "from-amber-200 via-orange-300 to-rose-400", shape: "rounded-t-full", tall: true },
{ name: "Harbour cup", maker: "Ren Takeda", price: "$32", art: "from-sky-200 via-sky-400 to-indigo-500", shape: "rounded-b-3xl", tall: false },
{ name: "Moss bowl", maker: "Carys Bell", price: "$58", art: "from-lime-200 via-emerald-400 to-teal-600", shape: "rounded-b-full", tall: false },
{ name: "Ember jug", maker: "Tomas Ruiz", price: "$96", art: "from-rose-300 via-red-400 to-orange-500", shape: "rounded-t-3xl rounded-b-lg", tall: true },
{ name: "Slate plate", maker: "Ines Marot", price: "$41", art: "from-slate-300 via-slate-500 to-slate-700", shape: "rounded-full", tall: false },
{ name: "Plum bottle", maker: "Kofi Mensah", price: "$72", art: "from-fuchsia-200 via-purple-400 to-violet-600", shape: "rounded-t-full rounded-b-2xl", tall: true },
];
type BlurFadeProps = {
children: ReactNode;
className?: string;
/** Seconds to wait before starting. Raise it item by item for a stagger. */
delay?: number;
duration?: number;
/** How far it travels, in pixels. */
offset?: number;
direction?: "up" | "down" | "left" | "right";
blur?: string;
/** Wait until the element scrolls into view. */
inView?: boolean;
};
/** Wrap anything in this to have it come into focus: it fades, slides a few pixels and sharpens from a blur. */
function BlurFade({ children, className, delay = 0, duration = 0.5, offset = 10, direction = "up", blur = "8px", inView = true }: BlurFadeProps) {
const ref = useRef<HTMLDivElement>(null);
const reduce = useReducedMotion();
const seen = useInView(ref, { once: true, margin: "-40px" });
const show = !inView || seen;
const axis = direction === "left" || direction === "right" ? "x" : "y";
const from = direction === "down" || direction === "right" ? -offset : offset;
if (reduce) return <div className={className}>{children}</div>;
return (
<motion.div
ref={ref}
className={className}
initial="hidden"
animate={show ? "visible" : "hidden"}
variants={{
hidden: { [axis]: from, opacity: 0, filter: `blur(${blur})` },
visible: { [axis]: 0, opacity: 1, filter: "blur(0px)" },
}}
transition={{ delay: 0.04 + delay, duration, ease: [0.22, 1, 0.36, 1] }}
>
{children}
</motion.div>
);
}
export default function BlurFadeInStaggeredContentReveal() {
// Changing the key remounts the content, which plays the reveal again.
const [run, setRun] = useState(0);
return (
<section className="bg-stone-100 px-5 py-16 sm:px-6 sm:py-24">
<div key={run} className="mx-auto max-w-6xl">
<div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
<div className="max-w-xl">
<BlurFade>
<p className="text-sm font-semibold uppercase tracking-widest text-orange-700">Kiln and Counter, autumn firing</p>
</BlurFade>
<BlurFade delay={0.08}>
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-stone-900 sm:text-5xl">Six pieces, straight from the kiln.</h2>
</BlurFade>
<BlurFade delay={0.16}>
<p className="mt-4 text-base leading-7 text-stone-600">Each one is thrown by hand in a studio you can visit. When a piece sells, the next firing decides what replaces it.</p>
</BlurFade>
</div>
<BlurFade delay={0.24}>
<button
type="button"
onClick={() => setRun((n) => n + 1)}
className="inline-flex h-11 items-center gap-2 rounded-full border border-stone-300 bg-white px-5 text-sm font-semibold text-stone-900 transition hover:border-stone-900 focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-600 focus-visible:ring-offset-2 focus-visible:ring-offset-stone-100"
>
<RotateCcw aria-hidden="true" className="h-4 w-4" />
Replay the reveal
</button>
</BlurFade>
</div>
<ul className="mt-10 grid grid-cols-2 gap-4 lg:grid-cols-3 lg:gap-6">
{pieces.map((p, i) => (
<li key={p.name}>
<BlurFade delay={0.3 + i * 0.09} duration={0.6} offset={16}>
<article className="overflow-hidden rounded-3xl bg-white shadow-sm ring-1 ring-stone-200">
<div aria-hidden="true" className="flex h-36 items-end justify-center bg-stone-50 sm:h-56">
<span className={`w-20 bg-gradient-to-b shadow-xl shadow-stone-900/20 sm:w-28 ${p.art} ${p.shape} ${p.tall ? "h-28 sm:h-44" : "h-16 sm:h-28"}`} />
</div>
<div className="flex flex-wrap items-baseline justify-between gap-x-3 p-4 sm:p-5">
<div>
<h3 className="font-semibold text-stone-900">{p.name}</h3>
<p className="text-sm text-stone-600">{p.maker}</p>
</div>
<p className="text-sm font-semibold text-stone-900">{p.price}</p>
</div>
</article>
</BlurFade>
</li>
))}
</ul>
</div>
</section>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/blur-fade-in-staggered-content-reveal.json
Each wrapped element starts with zero opacity, a CSS blur filter and a small offset. When it enters the viewport it animates to full opacity, no blur and its natural position. Blur and fade together read as the element coming into focus.
Give each item a delay based on its index, for example `delay={i * 0.08}`. Keep the total under about a second so the last item is not left waiting.
No. `useInView` is called with `once: true`, so each element animates the first time it appears and then stays put.
A blur filter is heavier than opacity or transform, but for short entrances on a handful of elements it is fine. On long lists, wrap rows or groups instead of every small element.
It does not. In that mode the wrapper skips the hidden state completely, so the content is simply there when the page loads.
Adapted from Blur Fade in Magic UI, used under its open-source licence. Licences and credits
Related: Word by Word Blur In Paragraph Reveal, Product Grid with Filter Chips, Blog Post Card Grid with Category Tags and Reading Time.
More scroll effects or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.