caliper uidocs

Image generation

Free

A card that shows an image as it is generated, from queued to complete.

Installs components/ui/image-generation.tsxDepends on @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/image-generation.json
Illustration of a sailboat in a quiet harbour

A quiet harbour at dusk, flat shapes, muted teal and clay

Image ready

When to use #

Use it for

  • An assistant produces images and the wait should feel alive.
  • You need retry and download actions on the result.

Skip it when

  • The image already exists and only needs viewing; use the image viewer.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/image-generation.json

Adds its 4 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above.

example.tsx
import { Download } from "lucide-react"
import { useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import { ImageGeneration, type GenerationStatus } from "@/components/ui/image-generation"

const stages = ["Composing the scene", "Adding detail", "Refining edges"];

const covers = ["/media/harbour.svg", "/media/terrace.svg", "/media/ridge.svg"];

export function ImageGenerationDemo() {
  const [status, setStatus] = useState<GenerationStatus>("done");
  const [progress, setProgress] = useState(1);
  const [cover, setCover] = useState(0);
  const [fail] = useState(false);
  const timers = useRef<ReturnType<typeof setTimeout>[]>([]);
  const ticker = useRef<ReturnType<typeof setInterval>>(undefined);
  const later = (ms: number, fn: () => void) => timers.current.push(setTimeout(fn, ms));
  const generate = () => {
      timers.current.forEach(clearTimeout);
      clearInterval(ticker.current);
      setStatus("queued");
      setProgress(0);
      later(900, () => {
        setStatus("generating");
        let p = 0;
        ticker.current = setInterval(() => {
          p = Math.min(1, p + 0.045);
          setProgress(p);
          if (fail && p > 0.6) {
            clearInterval(ticker.current);
            setStatus("failed");
          } else if (p >= 1) {
            clearInterval(ticker.current);
            setCover((c) => (c + 1) % covers.length);
            setStatus("done");
          }
        }, 160);
      });
    };
  const cancel = () => {
      timers.current.forEach(clearTimeout);
      clearInterval(ticker.current);
      setStatus("canceled");
    };
  return (
    <ImageGeneration
              prompt="A quiet harbour at dusk, flat shapes, muted teal and clay"
              alt="Illustration of a sailboat in a quiet harbour"
              status={status}
              src={covers[cover]}
              progress={status === "queued" ? undefined : progress}
              stage={stages[Math.min(2, Math.floor(progress * 3))]}
              onCancel={cancel}
              onRetry={generate}
              actions={
                <>
                  <Button variant="secondary" size="sm" onClick={generate}>Vary</Button>
                  <Button variant="ghost" size="sm" aria-label="Download image"><Download aria-hidden="true" /> Download</Button>
                </>
              }
            />
  );
}

API reference #

ImageGeneration

component
PropTypeDefaultDescription
prompt *string—
status *GenerationStatus—
srcstring—
altstring—Describe the picture for people who cannot see it.
progressnumber—0 to 1 when known; omit for an indeterminate wait.
stagestring—What it is doing now, such as "Adding detail".
aspectRatiostring"4 / 3"
errorTextstring"The image could not be made. Nothing was charged."
onRetry(() => void)—
onCancel(() => void)—
actionsReactNode—Buttons under a finished image, such as Download or Vary.
classNamestring—

Accessibility #

It exposes progressbar, alert and status semantics, with aria-busy, aria-hidden, aria-label, aria-valuemin, aria-valuemax and aria-valuenow kept in step with state. Changes that matter are announced through a live region.

No custom key handling; Tab and the native activation keys do the work.

Motion #

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.