Fully Headless UI

Build any UI — chat or not — on top of the CopilotKit primitives with zero UI opinions.

Not available for Agno yet

This feature (headless-complete) hasn't been tagged in any Agno cell yet. Try LangGraph (Python) instead, or browse the framework-agnostic version.

What is this?

A headless UI gives you full control over the chat experience — you bring your own components, layout, and styling while CopilotKit handles agent communication, message management, tool-call rendering, and streaming. No <CopilotChat>, no slot overrides, just your components composed on top of the low-level hooks.

When should I use this?

Use headless UI when:

  • The slot system isn't enough — you need a completely different layout.
  • You're embedding chat into an existing UI with its own patterns.
  • You're building a non-chat surface that still talks to an agent (a dashboard, a canvas, an inspector) and want useRenderToolCall / useRenderActivityMessage on their own.
  • You want to render generative UI primitives outside of a chat entirely.
Live Demo: LangGraph (Python)headless-completeOpen full demo →

The core hooks

Three hooks do the heavy lifting — they're the same primitives <CopilotChat> uses internally.

  • useAgent({ agentId }) — exposes the current conversation (messages, isRunning) and the run-state object.
  • useCopilotKit() — returns the runtime handle you call runAgent({ agent }) on.
  • useRenderToolCall() — returns a function that paints any registered tool call inline.

Minimal example

Start small — a hand-rolled message list and composer built from useAgent + useCopilotKit:

The message list is a plain .map() over agent.messages — user messages render as right-aligned bubbles, assistant messages render streamed text plus inline tool calls via renderToolCall({ toolCall }):

No <CopilotChat />, no slots. The trade-off: you only get text + tool calls. Reasoning messages, activity messages, and custom before/after slots won't show up unless you wire them in yourself — which is exactly what the complete example covers.

Complete example

The headless-complete cell rebuilds the full generative-UI composition — text, tool calls, reasoning cards, A2UI + MCP Apps activity messages, custom before/after message slots — from the low-level hooks directly, without importing <CopilotChatMessageView>.

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:

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

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

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:

Next steps

  • Slots — less work than going fully headless, often enough.
  • CSS customization — when you just need to re-skin the defaults.