--- title: Headless UI icon: "lucide/Code" description: Build fully custom chat interfaces with complete rendering control via hooks. hideTOC: true snippet_cell: headless-complete --- ## Full rendering control via hooks CopilotKit's headless hooks give you complete control over the chat experience: you compose messages, streaming, and tool-call surfaces yourself with zero UI opinions. Bring your own design system and render everything your way. There are two live cells on this page. Start with [**Minimal**](#minimal-headless-simple) for the smallest possible custom chat on `useAgent` + `useCopilotKit`, then jump to [**Complete**](#complete-headless-complete) to see the full generative-UI composition (tool calls, reasoning, activity messages, custom before/after slots) rebuilt by hand from the low-level hooks. ## When should I use this? Use headless UI when you want to: - Build a completely custom chat interface with your own design system - Integrate agent chat into existing UI patterns - Have full control over message rendering and interaction - Drop generative UI primitives (`useRenderToolCall`, `useRenderActivityMessage`, `useRenderCustomMessages`) into a layout that isn't a chat at all ## Minimal (`headless-simple`) The bare minimum: three hooks do the heavy lifting. - `useAgent({ agentId })` exposes the current conversation (`messages`, `isRunning`) and the run-state object. - `useCopilotKit()` returns the runtime handle you call `runAgent({ agent })` on (the same entry point `` uses internally). - `useComponent(...)` (sugar over `useFrontendTool`) lets you register a React component the agent can render by invoking a named tool call. `useRenderToolCall()` then returns a function that paints any tool call inline. The message list is a plain `.map()` over `agent.messages`: user messages render as right-aligned bubbles, assistant messages render any streamed text plus inline tool calls via `renderToolCall({ toolCall })`: That's it: no ``, no ``, no slots. The downside: you only get text + tool calls. Reasoning messages, activity messages (A2UI, MCP Apps), and custom before/after slots won't show up unless you wire them in yourself, which is exactly what the next section covers. ## Complete (`headless-complete`) This is the heart of the page. The `headless-complete` cell rebuilds the **full** generative-UI weave (text, tool calls via `useRenderTool` / `useDefaultRenderTool` / `useComponent` / `useFrontendTool`, reasoning cards, A2UI + MCP Apps activity messages, and custom before/after message slots) from the low-level hooks directly, without importing `` or ``. ### The `useRenderedMessages` hook The cell's central piece is a hand-rolled `useRenderedMessages(messages, isRunning)` that returns the same flat list of messages, each augmented with a `renderedContent: ReactNode` field. This hook is a **manual recreation of what `` does**; compare it line-for-line against the `renderMessageBlock` helper inside the canonical primitive: [`packages/react-core/src/v2/components/chat/CopilotChatMessageView.tsx:542-612`](https://github.com/CopilotKit/CopilotKit/blob/main/packages/react-core/src/v2/components/chat/CopilotChatMessageView.tsx#L542-L612). Three low-level hooks feed it: - `useRenderToolCall()` — returns the renderer for any registered tool call (per-tool via `useRenderTool` / `useComponent`, plus the wildcard from `useDefaultRenderTool`). - `useRenderActivityMessage()` — renders A2UI + MCP Apps activity messages for the current agent scope. - `useRenderCustomMessages()` — invokes `renderCustomMessage` hooks registered against the active `CopilotChatConfigurationProvider`, emitting `"before"` and `"after"` slots around every message. ### Per-role dispatch Inside `renderMessageContent` the role-switch mirrors `CopilotChatMessageView`'s `renderMessageBlock` exactly: assistant bodies get text + tool calls, user bodies get their text content, reasoning messages go through the `` leaf component, and activity messages route through `renderActivityMessage`: ### Tool-call composition For each `toolCall` on an assistant message, we look up the sibling `tool`-role message (keyed by `toolCallId`) and hand both to `renderToolCall`. This mirrors `CopilotChatToolCallsView` exactly: ### Bubble chrome The `UserBubble` and `AssistantBubble` components are **pure chrome**: they receive the pre-rendered node from `useRenderedMessages` and drop it into a styled container. No chat primitives are imported here: