Image reveal
ProAn image generation canvas where a soft, blurred preview made from the image's own colors sharpens top to bottom behind a soft-edged band. It works the way ChatGPT, Midjourney and Gemini show images forming, with fine film grain, a translucent progress pill and calm queued and error states.
Quiet landscapes at golden hour
- Showing an image model's progress on a canvas or in a chat reply.
- A batch of image variations sampling side by side.
- A queued image job that should feel alive before it starts.
- Plain image loading from a CDN; use a blur-up placeholder or a skeleton.
- Text generation or tool calls; use text-shimmer or tool-call.
- Determinate file uploads; use progress.
Installation
Pro source and install commands unlock with a Pro plan.
import { ImageReveal } from "@/components/arc/image-reveal/image-reveal"; export function GeneratedImage({ job }: { job: { status: "queued" | "generating" | "done" | "error"; step: number; url?: string } }) { return ( <ImageReveal src={job.url} alt="A calm alpine lake at golden hour" status={job.status} progress={job.step / 30} steps={30} aspectRatio="4 / 5" onRetry={() => retry(job)} /> );}Estimated duration, no progress events
<ImageReveal src={url} status={url ? "done" : "generating"} duration={8000} />Batch of four with a Generate again button
const [run, setRun] = useState(0); <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}> {urls.map((url, index) => ( <ImageReveal key={index} src={url} size="sm" seed={index + 1} runKey={run} loop={false} duration={4200} style={{ maxInlineSize: "none" }} /> ))}</div><Button onClick={() => setRun(run + 1)}>Generate again</Button>ImageReveal
Image generation the way a diffusion model samples: pure latent noise, then one denoising step at a time the prediction resolves from color blobs to composition to shapes while the noise falls away, then a tiled decode lands the sharp image and the pill shows how long it took. With no props it plays a looping, simulated generation over sample photos.
srcstring–Final image URL. Cross-origin images work (pixels are never read back). Omit to cycle through sample photos.altstring"Generated image"Alt text for the final image, used as the canvas label once done.status"queued" | "generating" | "decoding" | "done" | "error"–Controlled state. Omit for auto mode, which runs queued, sampling over `duration`, decoding over `decodeDuration`, then done. Going straight from generating to done still plays the decode. Every change transitions smoothly.progressnumber–Controlled progress from 0 to 1 while generating, usually step / total. The canvas moves one denoising step at a time, like the real sampler. Omit to step at a constant rate over `duration`.durationnumber4600Milliseconds the sampling steps take when `progress` is not controlled.decodeDurationnumber560Milliseconds the tiled decode takes after the last step.stepsnumber30Sampling steps. Each is one visible denoising step, and the pill shows it as 18 / 30 (tabular numerals, reserved width).aspectRationumber | string1Width over height, as a number (1.5) or a CSS ratio ("4 / 5"). The image is cover-cropped to it.size"sm" | "md" | "lg""md"Overlay scale and corner radius. "sm" (grids, thumbnails) shows only the ring and 18 / 30; "md" and "lg" add the Denoising label. The canvas always renders at device pixel ratio.colorstring–Any CSS color for the progress ring. Defaults to the accent.showProgressbooleantrueShows the translucent progress pill. It fades out shortly after the image resolves.sheenbooleantruePlays one soft light sheen across the image when it resolves. Skipped with reduced motion.queueLabelstring"Queued"Chip label while queued, such as "Queued, 2 ahead".errorMessagestring"Generation failed"Message in the error state.onRetry() => void–Shows a Retry button in the error state.onStatusChange(status: ImageRevealStatus) => void–Called when the displayed status changes, including auto mode transitions.onComplete() => void–Called once per run when the final image is fully resolved and crisp.autoPlaybooleantrueAuto mode only: starts generating on mount.loopbooleantrueAuto mode only: after `holdDuration` the image blurs back out and generates again.startDelaynumber0Auto mode only: extra queue time in milliseconds before a run, for staggered grids.queueDurationnumber900Auto mode only: milliseconds in the queue before each run.holdDurationnumber2600Auto mode only: milliseconds the finished image holds before a loop restarts.runKeystring | number–Auto mode only: change it to blur back out and run again (a Generate again button).seednumber1Seeds the latent noise, so tiles in a batch differ and a replay of the same seed shows the same noise. Pass the model's real seed.classNamestring–Class on the root. The root is 100% wide with a 560px max width; override max-inline-size in grids.styleCSSProperties–Inline styles on the root.- Tab
- Moves to the Retry button in the error state. The canvas itself is not focusable.
- EnterorSpace
- Activates Retry.
- The canvas has role img; its label names the state while generating and becomes the alt text once done.
- A polite live region announces Queued, Generating image, Image ready and the error message, not every step. Auto mode appends simulated.
- While generating, the pill is a progressbar with aria-valuenow and an aria-valuetext of Step n of total.
- Status is never color alone: the pill pairs a ring or check with a short label, and the error state pairs an alert icon with its message.
- Queued is the calm surface tone with one broad, soft shimmer that starts and ends fully off canvas, so its 2.6s loop has no seam.
- Generating starts from pure latent noise: per-cell gaussian color at about 1/8 of the output resolution, drawn as crisp blocks, with a new seeded frame every step crossfaded in 35ms.
- Under the noise is the model's current prediction, built from the image itself (6 to 96 px wide copies, smoothly upscaled). It moves along that chain per step, so color blobs come first, then composition, then shapes.
- Noise strength follows a schedule that drops fast early, so the composition reads by about a third of the steps and the last steps only refine. Steps run at a constant rate, like a real sampler.
- After the last step a tiled decode lands the sharp image tile by tile in raster order, each tile crossfading in, and the pill switches to Decoding.
- One soft-light sheen sweeps across once at completion (1.1s), the pill shows the elapsed time, then fades out.
- Starting over or swapping the photo fades the decoded image straight back to the surface before anything resets underneath, and a photo swap crossfades from a snapshot, so there is never a hard cut.
- Pill labels crossfade (100ms out, 160ms in after a 60ms gap) and the pill width springs with no bounce.
- Reduced motion keeps one static noise frame, decodes as a single crossfade and drops the shimmer travel, spinner and sheen.
- The frame loop stops off screen (IntersectionObserver) and in background tabs, and draws nothing once the image is resolved.
- Fills its container width (up to 560px by default) and keeps the given aspect ratio.
- The canvas renders at device pixel ratio (capped at 2), so it stays crisp from a 120px thumbnail to a large canvas.
- Latent noise is sized in CSS pixels (about 6 px cells), so it reads as the same latent grid at every size.
- Canvas 2D only, no WebGL and no per-pixel work per frame: a few drawImage calls. Noise is generated once per step at latent resolution (a few thousand cells).
- The prediction chain is built once per image and size; decode tiles are direct copies from the sharp image.
- Frames stop drawing when the image is resolved and nothing changes, and the loop stops entirely off screen.
- Images are decoded off the main thread before use.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Use for image generation surfaces: an image canvas, an inline image in a chat reply, a variations grid, or an empty state while a first image renders.
- Wire a real job by passing status and progress (step / total), plus the job's seed. Pass decoding while the VAE decodes if the backend reports it. With no progress events, pass status only and an estimated duration.
- Use size="sm" in grids. A batch samples in lockstep, so give every tile the same duration and its own seed, and override max-inline-size through className or style.
- The preview is sampled from the final image, so pass src as soon as a preview or final URL exists; until it loads the tile stays in the calm queued look.
- For text or agent work in progress, pick a text shimmer or an agent status component instead.
The full library index for assistants is at /llms.txt.