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 open callback to activate the side panel.
  • Panel — the full content rendered inside ArtifactPanel, portaled into the ArtifactPortalTarget in 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
});
OptionTypeDescription
titlestring | (props: P) => stringPanel header title. Static string or derived from props.
preview(props: P, controls: ArtifactControls) => ReactNodeInline preview rendered in the chat message.
panel(props: P, controls: ArtifactControls) => ReactNodeContent 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.

On this page