---
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`)

<FrameworkSetup concept="programmatic-control-setup" />

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 `<CopilotChat />`
  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.

<Snippet region="use-agent-simple" cell="headless-simple" title="frontend/src/app/page.tsx — useAgent + useCopilotKit + useComponent" />

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 })`:

<Snippet region="message-list-simple" cell="headless-simple" title="frontend/src/app/page.tsx — message list" />

That's it: no `<CopilotChat />`, no `<CopilotChatMessageView>`, 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 `<CopilotChatMessageView>` or
`<CopilotChatAssistantMessage>`.

### 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 `<CopilotChatMessageView>` 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).

<Snippet region="use-rendered-messages-hook" title="frontend/src/app/use-rendered-messages.tsx — composition hook" />

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 `<CopilotChatReasoningMessage>` leaf
component, and activity messages route through
`renderActivityMessage`:

<Snippet region="manual-activity-message-rendering" title="frontend/src/app/use-rendered-messages.tsx — per-role dispatch" />

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

<Snippet region="manual-tool-call-rendering" title="frontend/src/app/use-rendered-messages.tsx — assistant + tool calls" />

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

<Snippet region="custom-bubbles" title="frontend/src/app/{user,assistant}-bubble.tsx — pure chrome" />

<IntegrationGrid path="headless" />
