Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
Quick Answer
How to fix Blurhash image placeholder issues — encoding with Sharp, decoding in React, canvas rendering, Next.js image placeholders, CSS blur fallback, and performance optimization.
The Problem
The blurhash string is generated but the placeholder doesn’t render:
import { Blurhash } from 'react-blurhash';
<Blurhash hash="LEHV6nWB2yk8pyo0adR*.7kCMdnj" width={300} height={200} />
// Empty or broken elementOr encoding an image returns an error:
import { encode } from 'blurhash';
// Error: Expected data of length 160000, got 0Or the decoded placeholder colors don’t match the original image:
Blurhash shows blue tones but the image is mostly redWhy This Happens
Blurhash encodes images into short strings (~20-30 characters) that represent a blurry placeholder. The encoding/decoding pipeline has specific requirements:
- Encoding needs raw pixel data, not a file —
blurhash.encode()expects aUint8ClampedArrayof RGBA pixels. Passing a file path, buffer, or image element directly doesn’t work. You must extract pixel data first (using Sharp on the server or Canvas in the browser). - Component X and Y control the detail level —
componentXandcomponentY(1-9) determine how many color components the hash captures. Too low (1,1) produces a single solid color. Too high (9,9) creates a long hash with minimal visual benefit. react-blurhashrenders to a canvas element — it needswidthandheightprops to size the canvas. Without them, the canvas has zero dimensions and is invisible.- The hash must be generated at the right resolution — encoding a 4000x3000 image is slow. Resize to ~32x32 pixels before encoding — the result is virtually identical because blurhash is inherently low-resolution.
Fix 1: Generate Blurhash on the Server
npm install blurhash sharp
# For React component:
npm install react-blurhash// lib/blurhash.ts — server-side encoding with Sharp
import sharp from 'sharp';
import { encode } from 'blurhash';
export async function generateBlurhash(imagePath: string): Promise<string> {
// Resize to small dimensions for fast encoding
const { data, info } = await sharp(imagePath)
.raw() // Raw pixel data
.ensureAlpha() // Ensure RGBA
.resize(32, 32, { fit: 'inside' })
.toBuffer({ resolveWithObject: true });
// Encode to blurhash string
const hash = encode(
new Uint8ClampedArray(data),
info.width,
info.height,
4, // componentX (1-9, recommended: 4)
3, // componentY (1-9, recommended: 3)
);
return hash;
// Returns something like: "LEHV6nWB2yk8pyo0adR*.7kCMdnj"
}
// From URL
export async function blurhashFromUrl(url: string): Promise<string> {
const response = await fetch(url);
const buffer = Buffer.from(await response.arrayBuffer());
const { data, info } = await sharp(buffer)
.raw()
.ensureAlpha()
.resize(32, 32, { fit: 'inside' })
.toBuffer({ resolveWithObject: true });
return encode(new Uint8ClampedArray(data), info.width, info.height, 4, 3);
}
// Batch generate for all images
import fs from 'fs';
import path from 'path';
export async function generateBlurhashesForDirectory(dir: string) {
const files = fs.readdirSync(dir).filter(f => /\.(jpg|jpeg|png|webp)$/i.test(f));
const results: Record<string, string> = {};
for (const file of files) {
const filePath = path.join(dir, file);
results[file] = await generateBlurhash(filePath);
}
return results;
}Fix 2: Render Blurhash in React
'use client';
import { Blurhash } from 'react-blurhash';
import { useState } from 'react';
// Basic placeholder component
function BlurImage({ hash, src, alt, width, height }: {
hash: string;
src: string;
alt: string;
width: number;
height: number;
}) {
const [loaded, setLoaded] = useState(false);
return (
<div style={{ position: 'relative', width, height, overflow: 'hidden', borderRadius: '8px' }}>
{/* Blurhash placeholder — visible until image loads */}
{!loaded && (
<Blurhash
hash={hash}
width={width}
height={height}
resolutionX={32}
resolutionY={32}
punch={1} // Increase/decrease color intensity (default: 1)
style={{ position: 'absolute', top: 0, left: 0 }}
/>
)}
{/* Actual image */}
<img
src={src}
alt={alt}
width={width}
height={height}
onLoad={() => setLoaded(true)}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
opacity: loaded ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
/>
</div>
);
}
// Usage
<BlurImage
hash="LEHV6nWB2yk8pyo0adR*.7kCMdnj"
src="/photos/landscape.jpg"
alt="Mountain landscape"
width={800}
height={600}
/>Fix 3: Decode Without react-blurhash (Canvas)
// Decode blurhash to canvas manually — smaller bundle
import { decode } from 'blurhash';
function BlurhashCanvas({ hash, width, height }: {
hash: string;
width: number;
height: number;
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Decode to pixel data
const pixels = decode(hash, width, height);
// Create ImageData and draw to canvas
const imageData = ctx.createImageData(width, height);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
}, [hash, width, height]);
return <canvas ref={canvasRef} width={width} height={height} />;
}
// Decode to data URL (for use as CSS background)
function blurhashToDataURL(hash: string, width = 32, height = 32): string {
const pixels = decode(hash, width, height);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d')!;
const imageData = ctx.createImageData(width, height);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
return canvas.toDataURL();
}Fix 4: Next.js Image with Blurhash Placeholder
// Build-time: generate blurhash during content processing
// scripts/generate-placeholders.ts
import { generateBlurhash } from '@/lib/blurhash';
import fs from 'fs';
async function main() {
const images = fs.readdirSync('public/images');
const placeholders: Record<string, string> = {};
for (const img of images) {
placeholders[img] = await generateBlurhash(`public/images/${img}`);
}
fs.writeFileSync(
'src/data/placeholders.json',
JSON.stringify(placeholders, null, 2),
);
}
main();// components/OptimizedImage.tsx
'use client';
import Image from 'next/image';
import { Blurhash } from 'react-blurhash';
import { useState } from 'react';
import placeholders from '@/data/placeholders.json';
function OptimizedImage({ src, alt, width, height }: {
src: string;
alt: string;
width: number;
height: number;
}) {
const [loaded, setLoaded] = useState(false);
const filename = src.split('/').pop() || '';
const hash = placeholders[filename];
return (
<div style={{ position: 'relative', width, height }}>
{hash && !loaded && (
<Blurhash hash={hash} width={width} height={height}
style={{ position: 'absolute', inset: 0 }} />
)}
<Image
src={src}
alt={alt}
width={width}
height={height}
onLoad={() => setLoaded(true)}
style={{ opacity: loaded ? 1 : 0, transition: 'opacity 0.3s' }}
/>
</div>
);
}
// Or use Next.js built-in blurDataURL (base64 approach)
// Generate a tiny base64 placeholder instead of blurhash
import sharp from 'sharp';
async function generateBase64Placeholder(src: string) {
const buffer = await sharp(src)
.resize(10, 10)
.blur()
.toBuffer();
return `data:image/jpeg;base64,${buffer.toString('base64')}`;
}
// Usage with next/image
<Image
src="/photos/hero.jpg"
alt="Hero"
width={1200}
height={600}
placeholder="blur"
blurDataURL={base64Placeholder} // Tiny base64 string
/>Fix 5: CSS-Only Blur Fallback
// When you can't use canvas (SSR, email, etc.)
// Use the blurhash to extract dominant color as fallback
import { decode } from 'blurhash';
function getDominantColor(hash: string): string {
// Decode to a single pixel to get average color
const pixels = decode(hash, 1, 1);
const r = pixels[0];
const g = pixels[1];
const b = pixels[2];
return `rgb(${r}, ${g}, ${b})`;
}
// CSS fallback — just a colored background
function CSSBlurImage({ hash, src, alt }: {
hash: string;
src: string;
alt: string;
}) {
const bgColor = getDominantColor(hash);
const [loaded, setLoaded] = useState(false);
return (
<div style={{
backgroundColor: bgColor,
aspectRatio: '16/9',
borderRadius: '8px',
overflow: 'hidden',
}}>
<img
src={src}
alt={alt}
onLoad={() => setLoaded(true)}
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
opacity: loaded ? 1 : 0,
transition: 'opacity 0.5s',
}}
/>
</div>
);
}Fix 6: Store Blurhash in Database
// Schema — store hash alongside image reference
// Drizzle example
import { pgTable, text, integer } from 'drizzle-orm/pg-core';
export const images = pgTable('images', {
id: text('id').primaryKey(),
url: text('url').notNull(),
blurhash: text('blurhash'), // Store the hash string
width: integer('width').notNull(),
height: integer('height').notNull(),
});
// Generate on upload
async function handleImageUpload(file: File) {
const buffer = Buffer.from(await file.arrayBuffer());
// Upload to storage
const url = await uploadToStorage(buffer, file.name);
// Generate blurhash
const blurhash = await generateBlurhash(buffer);
const metadata = await sharp(buffer).metadata();
// Save to database
await db.insert(images).values({
id: crypto.randomUUID(),
url,
blurhash,
width: metadata.width!,
height: metadata.height!,
});
}
// Use in frontend
const imageData = await db.select().from(images).where(eq(images.id, id));
<BlurImage
hash={imageData.blurhash}
src={imageData.url}
width={imageData.width}
height={imageData.height}
alt="User photo"
/>Still Not Working?
react-blurhash renders empty — the width and height props are required and must be greater than zero. Also check that the hash string is valid — it should be 20-30 characters of alphanumeric characters plus ., :, /, +, -.
Encoding returns wrong colors — Sharp must output raw RGBA pixels (raw() + ensureAlpha()). If you skip ensureAlpha(), RGB data is misinterpreted as RGBA, shifting all colors. The Uint8ClampedArray must have exactly width * height * 4 bytes.
Encoding is slow — always resize the image to ~32x32 before encoding. The blurhash algorithm is O(width × height × componentX × componentY). A 4000x3000 image takes seconds; a 32x32 image takes microseconds.
Placeholder disappears before image loads — the onLoad event fires when the browser finishes loading the image. If the image is cached, onLoad fires immediately and the placeholder is never visible. Add a small minimum display time or accept that cached images skip the transition.
For related image optimization issues, see Fix: Sharp Not Working and Fix: Next.js Image Optimization Error.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AutoAnimate Not Working — Transitions Not Playing, List Items Not Animating, or React State Changes Ignored
How to fix @formkit/auto-animate issues — parent ref setup, React useAutoAnimate hook, Vue directive, animation customization, disabling for specific elements, and framework integration.
Fix: Embla Carousel Not Working — Slides Not Scrolling, Autoplay Not Starting, or Thumbnails Not Syncing
How to fix Embla Carousel issues — React setup, slide sizing, autoplay and navigation plugins, loop mode, thumbnail carousels, responsive breakpoints, and vertical scrolling.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Lottie Not Working — Animation Not Playing, File Not Loading, or React Component Blank
How to fix Lottie animation issues — lottie-react and lottie-web setup, JSON animation loading, playback control, interactivity, lazy loading, and performance optimization.