Image
Image message parts with preview, loading states, actions, and a fullscreen view.
Installation
Install manually
npx shadcn@latest add "@assistant-ui/image"First time? Set up a runtime
Runtime components read their state from an assistant-ui runtime. Add one to an existing project:
npx assistant-ui@latest initThen wrap your app in a runtime provider:
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";
export default function App() {
const runtime = useChatRuntime({
transport: new AssistantChatTransport({ api: "/api/chat" }),
});
return (
<AssistantRuntimeProvider runtime={runtime}>
{/* your components */}
</AssistantRuntimeProvider>
);
}The installation guide covers new projects, templates, and API routes.
This component is composed from runtime primitives and has no standalone build.
Image renders one image message part: a preview that click-zooms into a fullscreen view, a loading placeholder while the pixels arrive, and an error state when generation is blocked. With a runtime it renders straight from a message's image part; standalone you hand it the part yourself.
Getting started
Render images from a message
Register Image as the Image renderer on MessagePrimitive.Parts.
import { Image } from "@/components/assistant-ui/elements/image";
import { MessagePrimitive } from "@assistant-ui/react";
function AssistantMessage() {
return (
<MessagePrimitive.Root>
<MessagePrimitive.Parts components={{ Image }} />
</MessagePrimitive.Root>
);
}The Thread element already renders both assistant and user image parts through Image, so installing @assistant-ui/thread gives you this without any of the above.
Standalone, Image is a plain function of one part object: no context, no runtime.
Render an image directly
import { Image } from "@/components/assistant-ui/elements/image";
export function Picture() {
return (
<Image
type="image"
image="https://example.com/photo.png"
filename="photo.png"
status={{ type: "complete" }}
/>
);
}Anatomy
<div data-slot="image-root" data-variant={variant} data-size={size}>
{/* status.type === "running": */}
<div data-slot="image-generating" />
{/* status.type === "incomplete" && status.reason === "content-filter": */}
<div data-slot="image-content-filter-error" />
{/* otherwise: */}
<div className="aui-image-zoom-trigger" role="button" aria-label="Click to zoom image">
<div data-slot="image-preview">
<div data-slot="image-preview-loading" /> {/* until loaded or errored */}
<img /> {/* or, on error: */}
<div data-slot="image-preview-error" />
</div>
</div>
<span data-slot="image-filename" /> {/* omitted when there is no filename */}
<div data-slot="image-source" /> {/* optional attribution footer */}
</div>The three top-level states are exclusive and resolve from status: running shows the generating placeholder and the filename, incomplete with reason content-filter shows only the blocked-image message, and everything else (including no status) shows the real preview. Clicking the preview, or pressing Enter or Space while it has focus, opens the image in a fullscreen dialog: a portal overlay with a focus trap, Escape-to-close, click-outside-to-close, and the page scroll locked while it is open. Space activation prevents page scrolling. ImageActions (download, copy, regenerate) and ImageSource are not part of the default render tree; add them yourself, as shown below.
Examples
Attribute a tool result
Compose the image parts when a tool returns its own image and source metadata. ratio gives the preview a stable frame while it loads, and fit="cover" crops the image to fill that frame. Use fit="contain" when every pixel matters.
import { Image } from "@/components/assistant-ui/elements/image";
type ImageToolResult = {
imageUrl: string;
source: {
label?: string;
url?: string;
iconUrl?: string;
};
};
function ToolImage({ imageUrl, source }: ImageToolResult) {
return (
<Image.Root>
<Image.Zoom src={imageUrl}>
<Image.Preview src={imageUrl} ratio="16:9" fit="cover" />
</Image.Zoom>
<Image.Source {...source} />
</Image.Root>
);
}Image.Source uses label, or the URL host when no label is supplied. A safe URL makes the label a new-tab link. Give it iconUrl to show a 16px favicon; without one, it uses the source initial.
Add download, copy, and regenerate actions
ImageActions renders a download button and a copy-to-clipboard button; passing onRegenerate adds a third button that shows a spinner while it runs. Download picks a file extension from the image's MIME type (defaulting to .png) and uses part.filename when set; copy requires a browser with the Clipboard API and silently no-ops if it is unavailable or the write fails.
import { Image } from "@/components/assistant-ui/elements/image";
function ImageWithActions(part: React.ComponentProps<typeof Image>) {
return (
<Image.Root>
<Image.Zoom src={part.image}>
<Image.Preview src={part.image} />
</Image.Zoom>
<Image.Actions part={part} onRegenerate={() => refetchImage(part)} />
</Image.Root>
);
}Variants and sizes
variant and size are props of Image.Root. Image itself renders the defaults.
<Image.Root variant="ghost" size="lg">
<Image.Preview src={part.image} />
</Image.Root>| Variant | Description |
|---|---|
outline | Border around the frame (default) |
ghost | No border |
muted | Muted background while loading |
| Size | Max width |
|---|---|
sm | 16rem |
default | 24rem (default) |
lg | 32rem |
full | 100% of the container |
API reference
Image
| Export | Renders | Notes |
|---|---|---|
Image | div | The Image message-part renderer. Pass to MessagePrimitive.Parts as components.Image. |
Image.Root | div | Frame with variant and size. |
Image.Preview | div + img | Loads src, shows a placeholder until it does, an error icon if it fails. ratio and fit control fixed framing. |
Image.Source | div | Optional source attribution footer with an icon and label or host. |
Image.Zoom | trigger + portal dialog | Wraps children in a click-to-zoom trigger for src. |
Image.Filename | span | Renders nothing when there is no filename. |
Image.Generating | div | Shown while status.type === "running". |
Image.ContentFilterError | div | Shown when status is incomplete with reason content-filter. |
Image.Actions | div | Download, copy, and (if onRegenerate is passed) regenerate buttons. Not rendered by default. |
Image part
| Field | Type | Description |
|---|---|---|
image | string | A URL or data URI for the image. |
filename | string | Optional; shown under the preview and used as the download name. |
status | MessagePartStatus | Drives the generating / blocked / normal states. |
Image props
| Prop | Type | Default | Description |
|---|---|---|---|
type | "image" | required | Required by the part type. |
image | string | required | A URL or data URI for the image. |
filename | string | Shown under the preview and used as the download name. | |
status | MessagePartStatus | Drives the generating / blocked / normal states; omit for the normal state. | |
variant | "outline" | "ghost" | "muted" | "outline" | Frame style. A prop of Image.Root, not of Image. |
size | "sm" | "default" | "lg" | "full" | "default" | Max width. A prop of Image.Root, not of Image. |
Image.Root, Image.Preview, Image.Source, Image.Zoom, Image.Filename, Image.Generating, Image.ContentFilterError, and Image.Actions are exported individually for a custom layout, as shown in the examples above.
ImagePreview
| Prop | Type | Default | Description |
|---|---|---|---|
ratio | "auto" | "1:1" | "4:3" | "16:9" | "9:16" | "auto" | Keeps the image's natural height, or creates a fixed aspect-ratio frame. |
fit | "cover" | "contain" | "contain" | Crops to fill a fixed frame, or keeps the full image visible. |
ImageSource
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | Attribution label. Falls back to the URL host. | |
url | string | Safe URLs make the label a new-tab link and provide the fallback host. | |
iconUrl | string | 16px favicon URL. Without one, the source initial appears in a muted square. | |
className | string | Merged onto the attribution footer. |
All other div props are forwarded to Image.Source. It renders nothing without a label or a displayable URL host.