LQIP and Blur-Up Placeholders Without Layout Shift
Implement lqip placeholder and blur-up image loading in production without causing layout shift, using aspect-ratio containers and URL-based blur placeholders.
LQIP—Low-Quality Image Placeholder—is a technique where a blurred low-resolution version of an image loads first, giving users a sense of the final image while the full-resolution version downloads. Done incorrectly, LQIP introduces its own layout shift; done correctly, it improves perceived performance significantly.
This guide covers the aspect-ratio container requirement, generating blur placeholders from URLs, CSS filter transitions, and when to choose LQIP over a skeleton loader or a static color swatch.
What is LQIP
How LQIP placeholders work and why they improve perceived performance
LQIP works by showing a tiny version of the final image—typically 20–40 pixels wide—blurred and scaled up to fill the image slot. The blurred version communicates the shape, color palette, and rough composition of the final image before it loads. Users perceive this as faster loading because they see recognizable content immediately rather than a blank space.
The technique is used by Medium, Facebook, Pinterest, and most modern image-heavy applications. Next.js Image component implements a version of it with the placeholder='blur' prop. The blurred version is usually a base64-encoded inline data URI or a very small remote URL.
The tradeoff is complexity: you need a low-quality version of each image, a CSS filter transition, and a JavaScript event to remove the blur on load. URL-based placeholder services simplify the first step by generating an approximate blur placeholder from a URL alone.
Layout shift danger
How LQIP causes CLS if you skip the aspect-ratio container
The most common LQIP mistake is loading the blurred placeholder as an absolutely-positioned overlay without reserving the final image's dimensions beforehand. When the full-resolution image loads at its intrinsic size, the layout shifts around it.
The fix is an aspect-ratio container. The container reserves the correct space at the right ratio before either the placeholder or the final image loads. The placeholder and the final image both fill the container using position: absolute and object-fit: cover. This way, nothing in the document flow changes when the image swaps.
Setting explicit width and height on the img element achieves a similar result when the img is not wrapped in a container. The browser uses the width/height attributes to compute an intrinsic size and reserves the space. This is simpler but less flexible for responsive layouts where the width is fluid.
CSS setup
The aspect-ratio container pattern for LQIP placeholders
The container uses position: relative and a fixed or responsive aspect-ratio. The img fills the container with position: absolute, inset: 0, and object-fit: cover. The blur filter starts at 20px and transitions to 0 when a loaded class is added on the image's load event.
Keep the transition short—200–400ms is enough to signal the swap without feeling sluggish. Longer transitions are distracting in grids where many images load in sequence.
.lqip-wrap {
position: relative;
aspect-ratio: 4/3;
overflow: hidden;
background: #f3f4f6;
}
.lqip-wrap img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
filter: blur(20px);
transform: scale(1.05); /* hide blur edge artifacts */
transition: filter 0.3s ease, transform 0.3s ease;
}
.lqip-wrap img.loaded {
filter: blur(0);
transform: scale(1);
} Blur URL
Generating LQIP blur placeholders via URL
The fallback.pics blur route generates a blurred placeholder at any dimension. Use it as the initial src to get a blurred image that fills the container before the final image loads. Unlike base64-encoded inline data URIs, the URL is cacheable and reusable across multiple pages.
The color of the blur placeholder approximates the average color of a typical image in your catalog. For product images, a light gray matches most backgrounds. For hero images and editorial photos, you can use a custom background color that matches your typical palette.
<!-- HTML: blur placeholder swapped out on load -->
<div class="lqip-wrap">
<img
src="https://fallback.pics/api/v1/blur/800x600"
data-src="/final-image.jpg"
alt="Product photo"
loading="lazy"
onload="this.classList.add('loaded'); if(this.dataset.src){this.src=this.dataset.src}"
/>
</div> JavaScript swap
The image load event and src swap sequence
The src swap uses a simple onload handler. When the placeholder finishes loading, check for a data-src attribute and replace the src with the real image URL. Add the loaded class after the swap so the CSS transition fires.
The sequence matters. Set src to the placeholder, define data-src as the real URL, and only swap to the real URL inside the load callback. Trying to preload the real image and the placeholder simultaneously can cause race conditions where the wrong image ends up displayed.
In React, use the onLoad event prop and useState to track the loaded state. Set the initial state to the blur URL and transition to the real URL in the onLoad callback, wrapping the final image in a CSS class that controls the filter.
// React LQIP implementation
import { useState } from 'react';
function LQIPImage({ src, blurUrl, alt, width, height }) {
const [loaded, setLoaded] = useState(false);
const [currentSrc, setCurrentSrc] = useState(blurUrl);
return (
<div style={{ position: 'relative', aspectRatio: `${width}/${height}` }}>
<img
src={currentSrc}
alt={alt}
width={width}
height={height}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
filter: loaded ? 'blur(0)' : 'blur(20px)',
transform: loaded ? 'scale(1)' : 'scale(1.05)',
transition: 'filter 0.3s ease, transform 0.3s ease',
}}
onLoad={() => {
if (!loaded) {
setCurrentSrc(src);
setLoaded(true);
}
}}
/>
</div>
);
} When to use
Choosing LQIP vs skeleton loader vs static color swatch
LQIP works best for editorial content, hero images, and photography-heavy pages where the image composition is the primary content. The blurred preview gives users a recognizable glimpse of the final image before it loads.
Skeleton loaders work better for UI elements where the image is part of a larger layout—product cards, user avatars, news feed items. The skeleton communicates that content is being fetched without suggesting what the content looks like.
Static color swatches (a plain colored rectangle) are the lightest option and work well in fast-loading grids where the gap is too short to warrant animation. They are also the safest option for accessibility since they do not add motion and require no JavaScript.
Key takeaways
What to standardize before shipping
- Always use an aspect-ratio container or explicit width/height attributes to prevent layout shift during the blur-to-real swap.
- Scale the blurred placeholder to 1.05x and trim with overflow: hidden to hide blur edge artifacts at image borders.
- Use https://fallback.pics/api/v1/blur/{w}x{h} for a cacheable, reusable LQIP placeholder without base64 encoding.
- Add the CSS filter and transform transition only after the final image loads to avoid animating the blur placeholder.
- Prefer skeleton loaders for UI components and LQIP for editorial photography—the tradeoff is clarity of loading state vs. content preview.
Production fallback layer
Use fallback.pics anywhere an image URL is accepted.
Start with one deterministic URL and standardize fallback behavior across your design system.