Artifacts
Add side-panel content that opens from inline previews in chat.
Artifacts let a component render a compact inline preview inside the chat message and expand into a full side panel when clicked. Use them for code viewers, document previews, embedded frames, or any content that benefits from a larger canvas.
import { defineComponent } from "@openuidev/react-lang";
import { Artifact } from "@openuidev/react-ui";
import { z } from "zod";
const ArtifactCodeBlock = defineComponent({
name: "ArtifactCodeBlock",
props: z.object({
language: z.string(),
title: z.string(),
codeString: z.string(),
}),
description: "Code block that opens in the artifact side panel",
component: Artifact({
title: (props) => props.title,
preview: (props, { open, isActive }) => (
<CodeChip title={props.title} language={props.language} onClick={open} isActive={isActive} />
),
panel: (props) => (
<SyntaxHighlighter language={props.language}>{props.codeString}</SyntaxHighlighter>
),
}),
});How it works
An artifact component has two parts:
- Preview — a compact element rendered inline in the chat message. It receives an
opencallback to activate the side panel. - Panel — the full content rendered inside
ArtifactPanel, portaled into theArtifactPortalTargetin your layout. Only one panel is visible at a time.
Artifact() is a factory function that wires these together. It generates a ComponentRenderer that handles ID generation, artifact state, and panel portaling internally. Pass the result as the component field of defineComponent.
Artifact() config
import { Artifact } from "@openuidev/react-ui";
Artifact({
title, // string | (props) => string
preview, // (props, controls) => ReactNode
panel, // (props, controls) => ReactNode
panelProps, // optional — className, errorFallback, header
});| Option | Type | Description |
|---|---|---|
title | string | (props: P) => string | Panel header title. Static string or derived from props. |
preview | (props: P, controls: ArtifactControls) => ReactNode | Inline preview rendered in the chat message. |
panel | (props: P, controls: ArtifactControls) => ReactNode | Content rendered inside the side panel. |
panelProps | { className?, errorFallback?, header? } | Optional overrides forwarded to ArtifactPanel. |
Both preview and panel receive the full Zod-inferred props as the first argument and ArtifactControls as the second.
ArtifactControls
The controls object passed to preview and panel render functions.
interface ArtifactControls {
isActive: boolean; // whether this artifact's panel is currently open
open: () => void; // activate this artifact
close: () => void; // deactivate this artifact
toggle: () => void; // toggle open/close
}The preview typically uses open and isActive to show a click-to-expand button. The panel can use close to render a dismiss button inside the panel body.
Layout setup
Built-in layouts (FullScreen, Copilot, BottomTray) mount ArtifactPortalTarget automatically. Artifact panels render into this target with no extra setup.
If you build a custom layout with the headless hooks, mount one ArtifactPortalTarget in your layout where the panel should appear.
import { ArtifactPortalTarget } from "@openuidev/react-ui";
function Layout() {
return (
<div className="flex h-screen">
<main className="flex-1">{/* chat area */}</main>
<ArtifactPortalTarget className="w-[480px]" />
</div>
);
}Only one ArtifactPortalTarget should be mounted at a time. All artifact panels portal into this single element.
Headless hooks
For custom layouts or advanced control, use the artifact hooks from @openuidev/react-headless.
useArtifact(id)
Binds a component to a specific artifact by ID. Returns activation state and actions.
import { useArtifact } from "@openuidev/react-headless";
const { isActive, open, close, toggle } = useArtifact(artifactId);useActiveArtifact()
Returns global artifact state — whether any artifact is open, and a close action. Use this in layout components that resize or show overlays when any artifact is active.
import { useActiveArtifact } from "@openuidev/react-headless";
const { isArtifactActive, activeArtifactId, closeArtifact } = useActiveArtifact();Both hooks require a ChatProvider ancestor in the component tree.
Manual wiring
If Artifact() does not fit your use case, wire the pieces directly. This is the escape hatch for full control.
import { defineComponent } from "@openuidev/react-lang";
import { ArtifactPanel } from "@openuidev/react-ui";
import { useArtifact } from "@openuidev/react-headless";
import { useId } from "react";
const CustomArtifact = defineComponent({
name: "CustomArtifact",
props: CustomSchema,
description: "Artifact with full manual control",
component: ({ props }) => {
const artifactId = useId();
const { isActive, open, close } = useArtifact(artifactId);
return (
<>
<button onClick={open}>{isActive ? "Viewing" : "Open"}</button>
<ArtifactPanel artifactId={artifactId} title="Custom">
<div>{/* panel content */}</div>
</ArtifactPanel>
</>
);
},
});ArtifactPanel accepts artifactId, title, children, className, errorFallback, and header (boolean or custom ReactNode). It renders nothing when the artifact is inactive.