LQIP Image
Image component with Low Quality Image Placeholder (LQIP). Shows a blurred placeholder that fades into the full image on load.
Component
Remote URL mode (auto 2x2 placeholder)

Static import mode (full + placeholder)

"use client";
import Image, { type StaticImageData } from "next/image";
import { useState } from "react";
import { cn } from "@/lib/utils";
interface LqipImageProps {
src: string | StaticImageData;
placeholderSrc?: StaticImageData;
alt: string;
width?: number;
height?: number;
className?: string;
containerClassName?: string;
sizes?: string;
priority?: boolean;
fill?: boolean;
quality?: number;
}
function isStaticImageData(
src: string | StaticImageData,
): src is StaticImageData {
return typeof src === "object" && "src" in src;
}
export function LqipImage({
src,
placeholderSrc,
alt,
width,
height,
className,
containerClassName,
sizes,
priority = false,
fill = false,
quality,
}: LqipImageProps) {
const [loaded, setLoaded] = useState(false);
const srcString = isStaticImageData(src) ? src.src : src;
const hasAutoPlaceholder = !(
placeholderSrc || isStaticImageData(src)
);
const showPlaceholder =
placeholderSrc || hasAutoPlaceholder;
const placeholderImageSrc = hasAutoPlaceholder
? ({ height: 2, src: srcString, width: 2 } as {
src: string;
width: number;
height: number;
})
: placeholderSrc;
return (
<div
className={cn(
"relative overflow-hidden",
containerClassName,
)}
style={
fill
? {
height: "100%",
position: "relative",
width: "100%",
}
: width != null && height != null
? { height, width }
: undefined
}
aria-busy={!loaded}
role="img"
aria-label={alt}
>
{showPlaceholder && placeholderImageSrc && (
<Image
src={placeholderImageSrc}
alt=""
aria-hidden={true}
className={cn(
"absolute inset-0 size-full object-cover transition-opacity duration-700 ease-out",
loaded ? "opacity-0" : "opacity-100",
"scale-[1.1] blur-[20px]",
)}
fill={true}
sizes={sizes}
/>
)}
<Image
src={src}
alt={alt}
className={cn(
"object-cover transition-opacity duration-700 ease-out",
loaded ? "opacity-100" : "opacity-0",
fill ? "size-full" : undefined,
className,
)}
onLoad={() => setLoaded(true)}
loading={priority ? "eager" : "lazy"}
{...(fill
? { fill: true }
: { height: height ?? 600, width: width ?? 800 })}
{...(sizes !== undefined && { sizes })}
{...(quality !== undefined && { quality })}
/>
</div>
);
}Installation
pnpm dlx shadcn@latest add "https://pulkitxm.com/components/lqip-image.json"1. Copy the component file
"use client";
import Image, { type StaticImageData } from "next/image";
import { useState } from "react";
import { cn } from "@/lib/utils";
interface LqipImageProps {
src: string | StaticImageData;
placeholderSrc?: StaticImageData;
alt: string;
width?: number;
height?: number;
className?: string;
containerClassName?: string;
sizes?: string;
priority?: boolean;
fill?: boolean;
quality?: number;
}
function isStaticImageData(
src: string | StaticImageData,
): src is StaticImageData {
return typeof src === "object" && "src" in src;
}
export function LqipImage({
src,
placeholderSrc,
alt,
width,
height,
className,
containerClassName,
sizes,
priority = false,
fill = false,
quality,
}: LqipImageProps) {
const [loaded, setLoaded] = useState(false);
const srcString = isStaticImageData(src) ? src.src : src;
const hasAutoPlaceholder = !(
placeholderSrc || isStaticImageData(src)
);
const showPlaceholder =
placeholderSrc || hasAutoPlaceholder;
const placeholderImageSrc = hasAutoPlaceholder
? ({ height: 2, src: srcString, width: 2 } as {
src: string;
width: number;
height: number;
})
: placeholderSrc;
return (
<div
className={cn(
"relative overflow-hidden",
containerClassName,
)}
style={
fill
? {
height: "100%",
position: "relative",
width: "100%",
}
: width != null && height != null
? { height, width }
: undefined
}
aria-busy={!loaded}
role="img"
aria-label={alt}
>
{showPlaceholder && placeholderImageSrc && (
<Image
src={placeholderImageSrc}
alt=""
aria-hidden={true}
className={cn(
"absolute inset-0 size-full object-cover transition-opacity duration-700 ease-out",
loaded ? "opacity-0" : "opacity-100",
"scale-[1.1] blur-[20px]",
)}
fill={true}
sizes={sizes}
/>
)}
<Image
src={src}
alt={alt}
className={cn(
"object-cover transition-opacity duration-700 ease-out",
loaded ? "opacity-100" : "opacity-0",
fill ? "size-full" : undefined,
className,
)}
onLoad={() => setLoaded(true)}
loading={priority ? "eager" : "lazy"}
{...(fill
? { fill: true }
: { height: height ?? 600, width: width ?? 800 })}
{...(sizes !== undefined && { sizes })}
{...(quality !== undefined && { quality })}
/>
</div>
);
}2. Import and use
import { LqipImage } from "@/components/lqip-image";
<LqipImage
src="https://example.com/image.jpg"
alt="Description"
width={800}
height={400}
/>;Usage
Import the component
Add the LqipImage import to your file.
import { LqipImage } from "@/components/lqip-image";Remote URL
Use with a remote URL. The component auto-generates the blur placeholder.
<LqipImage
src="https://example.com/image.jpg"
alt="Description"
width={800}
height={400}
/>;Static import
For static imports, provide both full and placeholder images.
<LqipImage
src={fullImage}
placeholderSrc={placeholderImage}
alt="Description"
width={800}
height={400}
/>;Guidelines
- For remote URLs, the component fetches the same image at 2x2 pixels for the placeholder; no extra asset needed.
- For static imports, provide a small pre-generated placeholder (e.g. 20x20) as placeholderSrc for best results.
- Use priority for above-the-fold images to load eagerly.
- The container needs explicit dimensions when using fill; use a wrapper with aspect-ratio or fixed height.
Props
All props are optional unless marked required. Use these to customize every aspect of the component.
| Prop | Type | Default | Description |
|---|---|---|---|
| srcrequired | string | StaticImageData | — | Image source. String URL for remote; StaticImageData for static import. |
| placeholderSrc | StaticImageData | undefined | Placeholder image for static import mode. Required when src is StaticImageData. |
| altrequired | string | — | Accessible alt text for the image. |
| width | number | undefined | Image width in pixels. Omit when using fill. |
| height | number | undefined | Image height in pixels. Omit when using fill. |
| className | string | undefined | Additional CSS classes for the image element. |
| containerClassName | string | undefined | Additional CSS classes for the container. |
| sizes | string | undefined | Responsive sizes attribute for next/image. |
| priority | boolean | false | Load image eagerly (for above-the-fold images). |
| fill | boolean | false | Fill the container. Requires parent with explicit dimensions. |
| quality | number | undefined | Image quality (1-100). Passed to next/image. |
Examples
Remote URL
Remote URL mode. Auto-generates a tiny 2x2 placeholder from the same URL.
Remote URL mode (auto 2x2 placeholder)

Static import mode (full + placeholder)

import { LqipImage } from "@/components/lqip-image";
export function LqipImageBasic() {
return (
<LqipImage
src="https://example.com/image.jpg"
alt="Description"
width={800}
height={400}
/>
);
}Static import
Static import mode. Provide both full and placeholder images.
Remote URL mode (auto 2x2 placeholder)

Static import mode (full + placeholder)

import { LqipImage } from "@/components/lqip-image";
import fullImage from "@/assets/full.webp";
import placeholderImage from "@/assets/placeholder.webp";
export function LqipImageStatic() {
return (
<LqipImage
src={fullImage}
placeholderSrc={placeholderImage}
alt="Description"
width={800}
height={400}
/>
);
}Fill container
Use fill to make the image fill its container.
Remote URL mode (auto 2x2 placeholder)

Static import mode (full + placeholder)

import { LqipImage } from "@/components/lqip-image";
export function LqipImageFill() {
return (
<div className="relative h-64 w-full">
<LqipImage
src="https://example.com/image.jpg"
alt="Description"
fill
/>
</div>
);
}Last updated on Mar 13

