Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong
Part of: React & Frontend Errors
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.
A second class of failure is the runtime environment. blurhash itself is pure JavaScript with no native dependencies, but the path that produces RGBA pixels is environment-specific. On the server you reach for sharp, which is a native module; on the edge (Cloudflare Workers, Vercel Edge Functions) sharp is unavailable, so you have to use wasm-vips, @cf-wasm/photon, or a pre-computed hash stored in the database. On the browser you draw the image into a <canvas> and call getImageData, which fails if the image is cross-origin without proper CORS headers.
A third class is timing. The Blurhash placeholder is meant to display before the full image loads. If you generate the hash on-demand in the browser, the hash takes longer to produce than the full image takes to download from a CDN, so the placeholder is never useful. Hashes must be generated at build time or upload time and stored alongside the image URL.
Version History (Blurhash, plaiceholder, ThumbHash, and the placeholder ecosystem)
The placeholder-image space has moved fast since 2018, and which library you pick determines both visual quality and developer experience.
- Blurhash 1.0 (2018) — released by Wolt as a compact DCT-based encoder. The original blog post showed it being used in their food-delivery app to avoid empty grey boxes while photos loaded over slow mobile networks. The core algorithm has not changed materially since, but the surrounding packages have.
- react-blurhash and blurhash-python (2019) — official React component and a Python encoder, used to generate hashes on Django/Flask backends. The React component renders to canvas, so SSR shows a blank space until hydration.
- blurha.sh online generator (2019-2020) — a quick playground for testing hashes without writing code. Useful for prototyping, less useful in production because batch generation needs a script.
- plaiceholder (August 2021) — Joe Bell’s library bundled multiple placeholder strategies (Base64, Blurhash, SVG, CSS) behind one API. It uses Sharp under the hood and integrates with Next.js Image. plaiceholder is the most common path today for Next.js sites that want low-effort blur placeholders.
- Next.js
placeholder="blur"(built-in, 2021→) — Next.js Image accepts ablurDataURLprop, typically a 10-pixel Base64 JPEG. It is not Blurhash, but it solves the same problem with less code and ships in the framework. Many teams that adopted Blurhash in 2020-2021 have since switched. - ThumbHash (May 2023) — Evan Wallace’s alternative to Blurhash, designed to produce sharper, more recognizable placeholders at the cost of slightly longer strings. ThumbHash also preserves alpha. If you want better visual fidelity and don’t mind a ~25-character string instead of ~28, ThumbHash is usually the better choice today.
- Cloudflare Workers OG/placeholder generation (2023→) —
@cf-wasm/photonand Cloudflare Images can generate placeholders at the edge without Nodesharp. This matters for Cloudflare Pages, Vercel Edge, and Deno Deploy where native modules are unavailable.
Practical implication: in 2026, if you are starting fresh, evaluate ThumbHash or placeholder="blur" before reaching for Blurhash. If you already have Blurhash in production, the issues below still apply — but consider migrating only if visual quality is a complaint.
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.
Sharp fails to install on the deploy target — Cloudflare Workers, Vercel Edge Functions, and Deno Deploy do not support native modules. Generate the hash at build time on a Node.js worker, store it in a JSON file or the database, and ship only the decoder to the edge. The decoder is pure JS and works everywhere.
Hashes generated by different libraries don’t match — the Blurhash spec is fixed, but encoder implementations differ in how they handle alpha and gamma. If you generate a hash with blurhash-python and decode it with react-blurhash, colors may shift slightly. Stick to one ecosystem (JavaScript end-to-end is simplest).
Considering migration to ThumbHash — ThumbHash produces sharper placeholders and supports alpha. If your Blurhash placeholders look too washed out, ThumbHash is a near drop-in replacement with a different encoder/decoder pair. Migration requires re-encoding all stored hashes.
For related image optimization issues, see Fix: Sharp Not Working and Fix: Next.js Image Optimization Error. For related database storage patterns when persisting hashes alongside image metadata, see Fix: Drizzle ORM Not Working. For broader MDX-driven image workflows where placeholders are common, see Fix: MDX Not Working.
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.