Example image 1Example image 1
Example image 2Example image 2
Example image 3Example image 3
Example image 4Example image 4

nextjs-thumbhash

A ready to use ThumbHash implementation for Next.JS.
Show a placeholder while images are loading to avoid layout shift.

Installation

1. Install dependencies

npm install thumbhash sharp

2. Copy these files into your project:

    components/image.tsx

    "use client";
    import { DetailedHTMLProps, ImgHTMLAttributes, useState } from "react";
    import { thumbHashToDataURL } from "thumbhash";
    import base64ToUint8Array from "@/app/lib/base64ToUint8Array";
    import { cn } from "@/app/lib/cn";
    export type ImageProps = DetailedHTMLProps<
    ImgHTMLAttributes<HTMLImageElement>,
    HTMLImageElement
    >;
    export default function Image({
    thumbhash,
    ...props
    }: ImageProps & { thumbhash: string | null }) {
    const [loading, setLoading] = useState(true);
    return (
    <div className="relative h-full w-full">
    {loading && thumbhash && (
    // eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text
    <img
    {...props}
    className={cn(
    "absolute inset-0 h-full w-full object-cover transition-opacity duration-300 z-1",
    props.className
    )}
    src={thumbHashToDataURL(base64ToUint8Array(thumbhash))}
    />
    )}
    {/* eslint-disable-next-line @next/next/no-img-element, jsx-a11y/alt-text */}
    <img
    {...props}
    className={cn(
    "absolute inset-0 h-full w-full object-cover transition-opacity duration-300 z-2 opacity-100",
    loading && "opacity-0",
    props.className
    )}
    onLoad={(event) => {
    setLoading(false);
    props.onLoad?.(event);
    }}
    ref={(img) => {
    if (img?.complete) {
    setLoading(false);
    }
    }}
    />
    </div>
    );
    }

    lib/base64ToUint8Array.ts

    export default function base64ToUint8Array(base64: string): Uint8Array {
    const decodedString = atob(base64);
    const arrayBuffer = new Uint8Array(decodedString.length);
    for (let i = 0; i < decodedString.length; i++) {
    arrayBuffer[i] = decodedString.charCodeAt(i);
    }
    return arrayBuffer;
    }

    lib/thumbhash.ts

    import sharp from 'sharp';
    import { rgbaToThumbHash } from 'thumbhash';
    export async function generateThumbHash(arrayBuffer: ArrayBuffer) {
    const buffer = Buffer.from(arrayBuffer);
    const sharpImage = sharp(buffer).ensureAlpha();
    const resizedImage = sharpImage.resize(100, 100, {
    fit: 'inside',
    withoutEnlargement: true,
    });
    const { data, info } = await resizedImage.raw().toBuffer({ resolveWithObject: true });
    const rgba = new Uint8Array(data);
    const thumbHashUint8Array = rgbaToThumbHash(info.width, info.height, rgba);
    const thumbHashBase64 = Buffer.from(thumbHashUint8Array).toString('base64');
    return thumbHashBase64;
    }
    export default function base64ToUint8Array(base64: string): Uint8Array {
    const decodedString = atob(base64);
    const arrayBuffer = new Uint8Array(decodedString.length);
    for (let i = 0; i < decodedString.length; i++) {
    arrayBuffer[i] = decodedString.charCodeAt(i);
    }
    return arrayBuffer;
    }

Usage

Depending on your needs, you can either:

Option 1: Save ThumbHash in the DB

Here, you'll save the thumbhash in your database alongside the image URL. This is useful when you're already controlling the image URLs in your database.

1. Generate a thumbhash for your image:

import { generateThumbHash } from '@/lib/generateThumbHash';
// During image upload
async function handleImageUpload(file: File) {
const arrayBuffer = await file.arrayBuffer();
const imageUrl = await uploadImage(file);
const thumbhash = await generateThumbHash(arrayBuffer);
// Store thumbhash in your database
await saveToDatabase({
imageUrl,
thumbhash
});
}

2. Use the Image component:

import Image from '@/components/Image';
export default async function Page() {
const { imageUrl, thumbhash } = await getFromDatabase();
return (
<Image
src={imageUrl}
thumbhash={thumbhash}
alt="Description"
/>
);
}

Option 2: Use it with static images, with hardcoded thumbhashes

Original Image

ThumbHash Preview

import Image from '@/components/Image';
export default function Page() {
return (
<Image
src={imageUrl}
thumbhash={'YOUR_THUMBHASH'}
alt="Description"
/>
);
}

Here, you'll hardcode the thumbhash in your component. This is useful when you're using static images like a landing page.

How it Works

  1. When the component mounts, it shows a low-resolution placeholder generated from the thumbhash
  2. The main image loads in the background
  3. Once loaded, the main image fades in smoothly while the placeholder fades out
  4. The placeholder is removed from the DOM after the transition