Skip to content
Sign in
EffectsDither progress placeholderMenu

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.

Pro1 bit, then 4, 8 and 16 tones as the job advances, dithering a ThumbHash of the image, then a crossfade to the photo.

Installation

$ pnpm dlx shadcn@latest add @chatcn-pro/dither-progress-placeholder

Usage

tsx
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.

ProEach stage held still, the abstract field without a preview, blue noise and a failed job.

Props

PropTypeNotes
srcstringThe finished image. Setting it crossfades to it once decoded
altstringRequired. 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, totalStepsnumberThe step count the sampler reports. The most honest progress there is
progressnumber | null0 to 1, when the API reports a fraction. Leave all three out and it creeps
aspectRationumber | "3:2" | "16/9"Set it so nothing shifts when the image lands
labelReactNodeThe status chip label
showStatusbooleanShow the status chip. Default true
errorMessage, onRetryCopy for the error state (say what to do) and its Retry button
onRevealed() => voidOnce, when the finished image is fully on screen
maxPixels, rendererBacking store cap and "auto" | "webgl" | "2d"
previewstringWhat 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) => voidWhen the stage changes
stageLabel(levels) => stringWords for a tone count. Default "8 tones"
ink, paperstringDefault fg and bg tokens
seednumberSeeds the abstract field

Keyboard

KeyAction
TabReaches Retry in the error state. The placeholder itself takes no focus

Accessibility

  • The frame is aria-busy while the job runs and holds a screen reader progressbar with a value text such as Step 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: active the 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: more every stage stays at 1 bit in pure ink and paper.
  • Under reduced motion the crossfade is 150 ms. There is no loop to stop.