Building a Smart BlurImage Component in Next.js 15 App Router

Stop shipping blurry or broken images in Next.js
We've all been there:
- Images flash as blurry during load.
- Broken URLs silently fail.
- Fallback handling is a mess.
- UX takes a hit.
In this quick guide, i'll share a dead-simple reusable BlurImage
component i've been using in all my Next.js 14+ App Router projects. It handles:
✅ Blur-up placeholder while loading
✅ Fallback avatar if the image fails
✅ Fully typed, drop-in for any <Image />
component
The problem with image loading in production
When dealing with dynamic content (user profiles, external images, CMS sources), you can’t always trust the image URL.
Sometimes:
- The image takes time to load (slow networks)
- The image fails (broken links, expired URLs)
- Users see either a broken icon or nothing
The native <Image />
component in Next.js is great for optimization — but you still need a tiny UX layer on top.
The solution: BlurImage component
Let’s build a very simple wrapper around Next.js <Image />
using only App Router compatible code:
1"use client";
2
3import { useEffect, useState } from "react";
4import Image, { ImageProps } from "next/image";
5import { cn } from "@/lib/utils";
6
7export default function BlurImage(props: ImageProps) {
8 const [loading, setLoading] = useState(true);
9 const [src, setSrc] = useState(props.src);
10
11 useEffect(() => setSrc(props.src), [props.src]);
12
13 return (
14 <Image
15 {...props}
16 src={src}
17 alt={props.alt}
18 className={cn(
19 "rounded-lg object-cover object-center",
20 props.className,
21 loading ? "blur-[2px]" : "blur-0"
22 )}
23 onLoad={() => setLoading(false)}
24 onError={() => {
25 setSrc(`https://avatar.vercel.sh/${props.alt}`);
26 }}
27 />
28 );
29}
What’s happening here?
- loading state controls whether the blur effect is active.
- If the image loads, we simply turn off the blur.
- If loading fails, we swap in a fallback avatar from vercel.sh.
- Fully reusable for any image source.
"Note: cn() here is a utility to combine Tailwind classes — feel free to replace it with your own className merge function."
Why not use native placeholder="blur"?
Good question.
Next.js built-in blur placeholders work if you provide the blurDataURL. But for dynamic external URLs, you may not always have one available. This component gives you a consistent experience even when you can't precompute placeholders.
Usage example
Let’s say you’re rendering user avatars:
1import BlurImage from "@/components/ui-components/BlurImage";
2
3export function ProfilePage() {
4 return (
5 <main>
6 <div className="flex items-center gap-4">
7 <BlurImage
8 src="/profile-pic.jpg"
9 alt="User profile"
10 width={100}
11 height={100}
12 className="border-2 border-gray-200"
13 />
14 <div>
15 <h1>John Doe</h1>
16 <p>Software Engineer</p>
17 </div>
18 </div>
19 </main>
20 );
21}
✅ If user.profileImageUrl is valid: clean, sharp image after load ✅ If it fails: fallback avatar using user name ✅ Always smooth: initial blur transition
Small, simple, powerful
Sometimes it's these tiny components that:
- Save you hours of debugging broken image
- Give your app that "polished" feel
- Improve perceived performance with just a few lines of code
If you found this useful, follow the blog or join my newsletter for more practical Next.js patterns like this one. Also — feel free to share this with your dev friends who keep shipping broken images 😉