Blur up imagePro
An image that paints a ThumbHash placeholder in the first frame, in color, grayscale or a 1 bit dither, and crossfades to the photo once it is decoded.
On this page
Installation
Usage
import { BlurUpImage } from "@/components/ui/chatcn/pro/blur-up-image"
<BlurUpImage src={image.url} alt={image.alt} thumbhash={image.thumbhash} tone="grayscale" />Store a ThumbHash with each image at upload time: draw it at 100 px or less and call rgbaToThumbHash from @/lib/chatcn-pro-placeholders-thumbhash (a TypeScript port of ThumbHash by Evan Wallace, MIT). It is about 28 bytes. The component decodes it to a PNG data URL in plain JS, so it renders on the server and paints before any script runs, with the right colors and aspect ratio.
No ThumbHash? Pass any small data URL as placeholder (BlurHash output, a 16 px JPEG). grayscale keeps a monochrome page monochrome until the photo lands; dithered renders the placeholder as an ordered dither in ink and paper.
Examples
States
Waiting for the URL, an expired link with retry, a coarse dither, a loaded photo, a square crop of a 4:3 photo and right to left.
Props
| Prop | Type | Notes |
|---|---|---|
src | string | The photo. Leave it out while the URL is not ready yet |
alt | string | Required |
thumbhash | Uint8Array | string | ThumbHash bytes or base64 |
placeholder | string | Any small data URL, used when there is no thumbhash |
tone | "color" | "grayscale" | "dithered" | Default color |
algorithm, pixelSize | Dither pattern and cell size. Default bayer-4, 2 px | |
ink, paper | string | Dither tones. Default fg-subtle and inset tokens |
aspectRatio | number | "3:2" | Default width / height, then the ratio in the ThumbHash |
width, height, sizes, srcSet, loading, fetchPriority | Passed to <img> | |
errorText, retryLabel | string | Default "Image unavailable" and "Retry" |
onStatusChange | (status: "loading" | "loaded" | "error") => void | |
imgClassName | string | Classes for the photo |
Keyboard
| Key | Action |
|---|---|
Tab | Reaches Retry on the error chip. The image takes no focus |
Accessibility
- The placeholder is
aria-hiddenwith an emptyalt. The photo carries youralt; while there is nosrcyet, an emptyrole=imgwith the same name stands in. - The error chip is
role=alertand its Retry button has a 32 px hit area (44 px on touch) around the small visual. - Under
forced-colors: activethe photo shows at full opacity, with no fade.
Motion
- Only opacity moves: a 300 ms crossfade on the soft out curve, started after
img.decode()resolves. The blur radius never animates. - A photo already in the cache shows at once, with no fade and no placeholder flash.
- Once the fade ends the placeholder is removed from the DOM.
- Under reduced motion the crossfade is 150 ms.