Recipe: Claude-like artifacts UI
Build a split-pane chat interface with live-rendered artifacts — HTML, SVG, React components, and Mermaid diagrams — that update in real time as the model streams its response.
Overview
Claude's artifacts feature splits the conversation view into two resizable panes: a chat thread on the left and a live preview on the right. When the model emits structured content inside <antartifact> tags, the right pane renders it immediately. This recipe walks through the streaming parser, the artifact registry, and the resizable layout — all built with vanilla React and Tailwind CSS.
Prerequisites
- Next.js 14 App Router project with Tailwind CSS configured
- Streaming AI SDK endpoint (OpenAI-compatible or Anthropic)
- Basic understanding of React Server Components vs client boundaries
Step 1 — Streaming parser
The core of the system is a state machine that scans each streamed token for opening and closing artifact tags. When an <antartifact> tag is detected, the parser buffers content until the matching close tag. Everything outside the tags flows to the chat pane; everything inside flows to the artifact pane.
type ParserState = 'idle' | 'collecting';
function createArtifactParser() {
let state: ParserState = 'idle';
let buffer = '';
let artifactType = '';
return {
feed(chunk: string) {
// Scan for <antartifact type="...">
// Buffer content until </antartifact>
// Return { chat: string, artifact: string | null }
},
flush() { /* return remaining buffer */ }
};
}Step 2 — Artifact registry
Define a registry that maps artifact types to renderers. Each renderer receives the raw content string and returns a React node. Start with HTML, SVG, and Mermaid; add React component rendering later via a sandboxed iframe.
const artifactRenderers: Record<string, (content: string) => ReactNode> = {
'text/html': (c) => <iframe srcDoc={c} sandbox="allow-scripts" />,
'image/svg+xml': (c) => <div dangerouslySetInnerHTML={{ __html: c }} />,
'application/mermaid': (c) => <MermaidDiagram code={c} />,
};Step 3 — Resizable split pane
Use a CSS-grid layout with a draggable divider. The left pane holds the chat messages; the right pane holds the active artifact. Store the split ratio in local state and persist it to localStorage for session continuity.
<div className="grid h-full" style={{ gridTemplateColumns: `${leftPct}fr 4px ${rightPct}fr` }}>
<ChatPane messages={chatMessages} />
<div className="cursor-col-resize bg-[#8B5CF6]/20 hover:bg-[#8B5CF6]/40 transition-colors" />
<ArtifactPane content={activeArtifact} renderer={renderer} />
</div>Step 4 — Putting it together
Wire the parser into your streaming fetch loop. On each chunk, call parser.feed() and update both the chat accumulator and the artifact state. When the stream ends, call parser.flush() to drain any remaining buffer.
Result
You now have a Claude-style artifacts UI that streams structured content into a live preview pane. The parser handles nested tags gracefully, the registry makes it trivial to add new artifact types, and the resizable layout gives users full control over their workspace.