---
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: