Skip to content

Fix: Blurhash Not Working — Placeholder Not Rendering, Encoding Failing, or Colors Wrong

FixDevs ·

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 element

Or encoding an image returns an error:

import { encode } from 'blurhash';
// Error: Expected data of length 160000, got 0

Or the decoded placeholder colors don’t match the original image:

Blurhash shows blue tones but the image is mostly red

Why 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 fileblurhash.encode() expects a Uint8ClampedArray of 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 levelcomponentX and componentY (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-blurhash renders to a canvas element — it needs width and height props 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles