# Image Generation

> A frame for an image that is still being made: a grid of tiny dots that swell around the cursor, take on the picture's colors and dissolve into the finished image.

- Page: https://ui.devigner.cc/components/image-generation
- Category: Animation
- Requirements: React 18 or 19, Tailwind CSS v4
- Install: `npx devignerui add image-generation` (writes components/ui/image-generation.tsx)
- Packages it needs: cn, motion, @devigner-ui/icons, devignerui, thinking-orbs

## What the demo shows

The live demo plays one generation on a loop: a still grid of tiny dots fills the frame, faint while queued and full strength while generating, then the dots grow and take on the colors of the picture while refining, and finally grow into each other and fade as the sharp image comes in. Move the mouse over the grid and the dots around it swell and part a little; click to send a small ripple through them.

## When to use it

- The result slot of an AI image tool, from the moment the request is sent until the image arrives.
- Any long job that ends in a picture, such as a render, an upscale or a thumbnail being made on the server.
- Progress reported in a few steps (queued, generating, refining) rather than as a percentage.

## When to reach for something else

- A plain image that is only loading over the network. Use a skeleton or the browser's own lazy loading.
- Showing an exact percentage or time left. The swarm shows the stage, not how far along it is.
- Long grids of many results at once. Frames share one animation loop and one theme watcher, but each still draws its own canvas; a few on screen are fine, dozens moving together are not.

## Usage

```tsx
import { ImageGeneration } from "@/components/ui/image-generation";

<ImageGeneration status={status} src={url} prompt={prompt} />
```

Custom composition:

```tsx
<ImageGeneration status="error" aspectRatio="16 / 9" onRetry={retry} className="max-w-md" />
```

### Wired to a generation request

Shows each stage the server reports, then the finished image, with a retry on failure.

```tsx
import { useState } from "react";
import {
  ImageGeneration,
  type ImageGenerationStatus,
} from "@/components/ui/image-generation";

export function Generator({ prompt }: { prompt: string }) {
  const [status, setStatus] = useState<ImageGenerationStatus>("queued");
  const [url, setUrl] = useState<string>();

  async function run() {
    setStatus("generating");
    try {
      const res = await fetch("/api/generate", {
        method: "POST",
        body: JSON.stringify({ prompt }),
      });
      const { image } = (await res.json()) as { image: string };
      setUrl(image);
      setStatus("refining");
      setTimeout(() => setStatus("complete"), 1200);
    } catch {
      setStatus("error");
    }
  }

  return (
    <div className="flex flex-col items-start gap-3">
      <button type="button" onClick={run}>
        Generate
      </button>
      <ImageGeneration
        status={status}
        src={url}
        prompt={prompt}
        onRetry={run}
      />
    </div>
  );
}
```

### Wide frame

A 16:9 frame with no status line.

```tsx
import { ImageGeneration } from "@/components/ui/image-generation";

export function Banner({ src }: { src?: string }) {
  return (
    <ImageGeneration
      status={src ? "complete" : "generating"}
      src={src}
      aspectRatio="16 / 9"
      showStatus={false}
      className="max-w-xl"
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| status | `"queued" \| "generating" \| "refining" \| "complete" \| "error"` | generating | Where the generation is. The dots are faint while queued, full strength while generating, grow and take the image's colors while refining, and fade into the image when complete. |
| src | `string` | - | URL of the generated image. Shown when complete, and its colors tint the dots while refining. From another domain, set crossOrigin for the colors. |
| children | `ReactNode` | - | Your own media (Next Image, video) shown instead of an img of src. Pass tintSrc or src as well to keep the tinted dots. |
| tintSrc | `string` | src | Image the dots take their colors from, such as a small thumbnail, so the full image isn't downloaded just for its colors. |
| crossOrigin | `"anonymous" \| "use-credentials"` | - | CORS mode of the img of src and of the color read, kept the same so the image is fetched once. Set "anonymous" for an image on another domain served with CORS headers. |
| prompt | `string` | - | The prompt, shown in small text under the status line. |
| label | `string` | status text and prompt | Accessible name of the image frame, read by screen readers. |
| aspectRatio | `string` | 1 / 1 | Shape of the frame, as a CSS aspect-ratio such as "16 / 9". |
| focus | `{ x: number; y: number }` | { x: 50, y: 50 } | The point of the image kept in view when the frame crops it, in percent like object-position. The tinted dots follow the same crop. |
| interactive | `boolean` | true | Lets a mouse swell and gently part the dots around it, and a click send a small ripple through them, while the image is in progress. |
| statusText | `Partial<Record<status, string>>` | - | Replaces the built-in status text per status, such as { generating: "Drawing" }. Statuses left out keep the English default. |
| showStatus | `boolean` | true | Shows the status line with its icon under the frame. |
| onRetry | `() => void` | - | Shows a Try again button under the frame when status is "error", and is called when it is pressed. |
| retryLabel | `string` | Try again | Text of the retry button. |
| download | `boolean \| string` | - | Shows a download icon button in the frame's top-right corner when status is "complete" and src is set. A string is the saved file's name; true takes the name from the end of src. |
| downloadLabel | `string` | Download | Accessible name and tooltip of the download button. |
| classNames | `ImageGenerationClassNames` | - | Slot classes: frame, media and status. |

## Keyboard

| Keys | Action |
| --- | --- |
| Tab | Focuses the Try again button when status is "error" and onRetry is set, or the download button when status is "complete" and download is set. The frame itself is not focusable. |
| Enter / Space | Presses Try again or download. |

## Accessibility

- The frame has role="img" and is named by label, or by the status text and prompt ("Generating image: Glass spheres...").
- The root has aria-busy while the status is queued, generating or refining.
- A visually hidden aria-live="polite" region holds the status text, so each new status is read out once, even with showStatus off. The visible status line is aria-hidden so it isn't read twice.
- The canvas, the media layer and the status icon are aria-hidden. The status is always written out in text, so neither color nor motion is the only signal.
- Try again is a real <button type="button"> with a focus ring in the ring color. It appears without taking focus, so a failure never pulls a keyboard user away from where they are; the live region announces it.
- Download is an icon-only <button type="button"> over the frame's top-right corner, named by downloadLabel ("Download") with the same text as a tooltip and the same focus ring. It sits outside the role="img" frame so screen readers reach it, and also appears without taking focus.
- A prompt cut with an ellipsis shows in full as a tooltip on hover.
- Honors prefers-reduced-motion and an ancestor <MotionConfig reducedMotion="always">, so an in-app motion switch works too. Under reduced motion the dots jump straight to each state's size and color, the pointer neither swells nor moves them, a click sends no ripple and the status icon stands still.

## Theming

- The frame is bg-muted with rounded-xl corners; extra classes go through classNames.frame.
- The dots are drawn in the canvas's text color: the foreground token, or the destructive token when status is "error". They are read again whenever the class or data-theme attribute of <html> or the system color scheme changes, so a theme switch recolors them without a reload.
- The status mark sits in a 36px cell with no background: a 32px thinking orb while the job is in progress, a tick when complete, and an alert in the destructive color on error.
- While refining, each dot takes the color of the image under it, rounded to 4 bits a channel.
- The status line is text-foreground, text-destructive on error; extra classes go through classNames.status.

## Edge cases

- The dots sit on a hex grid that fills the frame, 12px apart, so a bigger frame has more of them.
- The canvas follows the frame's size; a resize, or moving the window to a screen with another pixel ratio, lays the dots out again where the current state wants them.
- The animation stops by itself once every dot has settled, and pauses while the frame is scrolled out of view.
- Coloring the dots needs to read the image's pixels. For an image from another domain, set crossOrigin="anonymous" and serve it with CORS headers (Access-Control-Allow-Origin); without them the dots stay one color and the image still shows when complete. The img of src uses the same mode, so the image is downloaded once.
- Colors are read only once the image has loaded, so the dots never flash black on the way. Transparent parts of an image leave their dots in the ink color.
- children replace the img of src as the visible media, so a Next Image works. The dots only take image colors when tintSrc or src is set as well; a small tintSrc thumbnail saves downloading the full image just for its colors. children that render nothing, such as {ready && <Image />} before it is ready, count as no media.
- Once the image is in progress no longer (complete or error), the dots stop catching the pointer, so a right-click, long-press or drag reaches the finished image.
- With no src and no children, complete leaves the tiny dots in place instead of fading them, since there is nothing to show under them.
- On error a small ripple runs out from the middle, then the dots settle back and dim to 45%. The image, if any, shows faintly and blurred, scaled a touch so the blur keeps clear of the frame's edges.
- If the pointer is over the frame when the status leaves the in-progress states, the swell around it lets go instead of staying where the cursor was.
- The grid never moves on its own; only the pointer moves dots off their places. A mouse or pen swells the dots within about a quarter of the frame's shorter side and gently parts the closest ones; a finger scrolls the page as usual. A click or tap sends a small ripple. None of this works once the status is complete or error.
- Download fetches src and saves it under the download name, so it works for an image on another domain only when that domain sends CORS headers (Access-Control-Allow-Origin). Without them the image opens in a new tab instead, where it can be saved by hand. It needs src; with only children there is nothing to save, so the button stays hidden.
- The frame is max-w-xs (320px) wide by default and its height follows aspectRatio. Pass a max-w-* or w-* class in className for another size.

## Troubleshooting

**The dots never take on the image's colors.**
The image is on another domain, so the browser won't let the canvas read its pixels unless it is served with Access-Control-Allow-Origin and crossOrigin="anonymous" is set. Or serve it from your own domain. The dots also stay one color when only children is passed; set tintSrc or src too.

**The frame is tiny or has no height.**
The height comes from aspectRatio and the width from the parent, up to max-w-xs. Inside a flex row with no width, give it a w-* class.

**The component renders with no background or the wrong colors.**
Colors use the standard shadcn/ui tokens only (background, foreground, primary, secondary, muted, accent, border, input, ring, destructive), so a project set up with shadcn/ui needs nothing extra and the component follows its theme, dark mode included. Without shadcn/ui, define those CSS variables for :root and .dark and map them in your Tailwind v4 @theme, or run npx shadcn init.
