UI Primitives

Headless building blocks for composing fully custom chat UIs

Beta — This feature is in alpha. APIs may change before stable release.

Low-level building blocks for composing custom chat UIs. The SDK handles all state, streaming, and context — you control the layout.

Two complementary APIs:

  • ChatPrimitives — named export of individual components, import under any alias
  • CopilotChat.* — same primitives mounted directly on CopilotChat for inline composition

All Primitives

import { ChatPrimitives as Chat } from "@yourgpt/copilot-sdk/ui";
PrimitiveDescription
Chat.MessageListRender-prop message list — reads messages from context
Chat.DefaultMessageFull SDK message bubble — use as fallback in custom lists
Chat.HeaderChat header bar
Chat.WelcomeWelcome screen shown when there are no messages
Chat.InputComposer / input box
Chat.ScrollAnchorAuto-scroll anchor, place at end of message list
Chat.MessageLow-level message row wrapper
Chat.MessageAvatarAvatar with fallback initials
Chat.MessageContentContent bubble — renders markdown, supports streaming
Chat.MessageActionsAction bar layout primitive (wraps action buttons)
Chat.MessageActionSingle action icon button with tooltip
Chat.LoaderStreaming / thinking indicator

Chat.MessageList props

interface MessageListProps {
  children?: (message: ChatMessage, index: number) => React.ReactNode;
  className?: string;
}

When children is provided, called once per message — return your custom component or fall back to Chat.DefaultMessage. When omitted, renders all messages with DefaultMessage.


Examples

Custom message type with fallback

import { ChatPrimitives as Chat } from "@yourgpt/copilot-sdk/ui";

<CopilotChat>
  <Chat.MessageList>
    {(message) =>
      message.metadata?.type === "plan" ? (
        <PlanCard key={message.id} message={message} />
      ) : (
        <Chat.DefaultMessage key={message.id} message={message} />
      )
    }
  </Chat.MessageList>
</CopilotChat>

Fully custom layout — compose from scratch

import { ChatPrimitives as Chat } from "@yourgpt/copilot-sdk/ui";

<CopilotChat>
  <div className="flex flex-col h-full">
    <Chat.Header />
    <Chat.Welcome />

    <div className="flex-1 overflow-y-auto px-4">
      <Chat.MessageList>
        {(message) => (
          <Chat.Message key={message.id} message={message}>
            <Chat.MessageAvatar message={message} />
            <Chat.MessageContent message={message} />
          </Chat.Message>
        )}
      </Chat.MessageList>
      <Chat.Loader />
      <Chat.ScrollAnchor />
    </div>

    <Chat.Input />
  </div>
</CopilotChat>

Mix primitives with message actions

import { ChatPrimitives as Chat } from "@yourgpt/copilot-sdk/ui";

<CopilotChat>
  {/* Register floating action buttons */}
  <CopilotChat.MessageActions role="assistant">
    <CopilotChat.CopyAction />
    <CopilotChat.FeedbackAction onFeedback={(msg, type) => log(msg.id, type)} />
  </CopilotChat.MessageActions>

  {/* Custom message list */}
  <Chat.MessageList>
    {(message) =>
      message.metadata?.type === "approval" ? (
        <ApprovalCard key={message.id} message={message} />
      ) : (
        <Chat.DefaultMessage key={message.id} message={message} />
      )
    }
  </Chat.MessageList>
</CopilotChat>

Per-message action buttons using primitives directly

import { ChatPrimitives as Chat } from "@yourgpt/copilot-sdk/ui";

<Chat.MessageList>
  {(message) => (
    <Chat.Message key={message.id} message={message}>
      <Chat.MessageAvatar message={message} />
      <div className="flex flex-col gap-1">
        <Chat.MessageContent message={message} />
        <Chat.MessageActions>
          <Chat.MessageAction
            icon={<CopyIcon />}
            tooltip="Copy"
            onClick={() => navigator.clipboard.writeText(message.content ?? "")}
          />
        </Chat.MessageActions>
      </div>
    </Chat.Message>
  )}
</Chat.MessageList>

messageView vs Chat.MessageList

Two ways to customize message rendering at different abstraction levels:

messageView propChat.MessageList
StyleProp on <CopilotChat>Child component inside <CopilotChat>
Accessmessages[] + pre-rendered messageElements[]messages[] via render-prop
When to useQuick overrides, inject extra UI around existing rendersFull layout control, building from primitives

Both are non-breaking and can coexist.

→ See Custom Message View for the messageView API.


Breaking Changes

None. Both are purely additive. Existing <CopilotChat /> usage is untouched.

On this page