Dither progress placeholderPro
A placeholder that gains information as the job advances: 1 bit, then 4, 8 and 16 tone ordered dithers with cells that get finer, then a crossfade to the photo.
On this page
Installation
Usage
import { DitherProgressPlaceholder } from "@/components/ui/chatcn/pro/dither-progress-placeholder"
<DitherProgressPlaceholder
src={job.imageUrl}
alt={job.prompt}
progress={job.progress}
preview={thumbHashToDataURL(job.thumbhash)}
/>Progress picks the stage: the four default stages split it evenly, so 30% is 4 tones and 80% is 16. Stages swap in one frame, so each reads as a step; only the final crossfade animates. What gets dithered is preview (a provider preview, a low resolution pass or a ThumbHash data URL from @/lib/chatcn-pro-placeholders-thumbhash), or an abstract field when there is none.
Pass your own stages ({ levels, cell }[]) for more or fewer steps, algorithm for another ordered pattern, and stageLabel to translate the tone count on the chip.
Examples
Stages and states
Each stage held still, the abstract field without a preview, blue noise with no progress reported, a failed job and right to left.
Props
| Prop | Type | Notes |
|---|---|---|
src | string | The finished image. Setting it crossfades to it once decoded |
alt | string | Required. Announced once the image is in |
status | "generating" | "done" | "error" | Default: done when src is set, otherwise generating. Going back to generating starts a new job |
step, totalSteps | number | The step count the sampler reports. The most honest progress there is |
progress | number | null | 0 to 1, when the API reports a fraction. Leave all three out and it creeps |
aspectRatio | number | "3:2" | "16/9" | Set it so nothing shifts when the image lands |
label | ReactNode | The status chip label |
showStatus | boolean | Show the status chip. Default true |
errorMessage, onRetry | Copy for the error state (say what to do) and its Retry button | |
onRevealed | () => void | Once, when the finished image is fully on screen |
maxPixels, renderer | Backing store cap and "auto" | "webgl" | "2d" | |
preview | string | What gets dithered. Without it, an abstract field |
stages | { levels: number; cell: number }[] | Default 2, 4, 8 and 16 tones at 4, 3, 2 and 2 px cells |
algorithm | "bayer-2" | "bayer-4" | "bayer-8" | "blue-noise" | Default bayer-8, which has the 64 thresholds 16 tones need |
onStageChange | (index, levels) => void | When the stage changes |
stageLabel | (levels) => string | Words for a tone count. Default "8 tones" |
ink, paper | string | Default fg and bg tokens |
seed | number | Seeds the abstract field |
Keyboard
| Key | Action |
|---|---|
Tab | Reaches Retry in the error state. The placeholder itself takes no focus |
Accessibility
- The frame is
aria-busywhile the job runs and holds a screen reader progressbar with a value text such asStep 18 of 30. Ticks are never announced. - A polite live region says
Image ready: <alt>once the image is in, and that the job failed when it fails. - The status chip is decorative (
aria-hidden); the progressbar carries the same facts. - The error state is
role=alert, says how to fix the problem and has a real Retry button. Small tiles keep an icon only retry with its name. - Under
forced-colors: activethe effect hides and the plain image shows. - The progressbar value text names the stage too, for example
62%, 8 tones.
Motion
- Stages are held, not blended. The only animation is the 200 ms crossfade to the photo, which starts after
img.decode()so it never shows a half painted image. - Under
prefers-contrast: moreevery stage stays at 1 bit in pure ink and paper. - Under reduced motion the crossfade is 150 ms. There is no loop to stop.