← All proposals

Chat Message

ChatMessage, with sender: user or agent.

Net-new componentOpen in Storybook

proposals/ChatMessage/HANDOFF.md73 lines · View on GitHub

Handoff: ChatMessage

What and why

One turn in an AI chat or agent conversation. PDS has no component for it. The user's message (a filled bubble on the right) and the agent's reply (plain content across the full width) are the same kind of thing, so it is one component with two variants, plus a thin ChatMessageList that stacks turns and owns the gap between them. Figma: Chat Message page (first sent 2026-10-19; the designer has since replaced the captures with a hand-built ChatMessage component set, Sender variants, in a section named "Chat message"). Context: context.md.

Exploration and decisions

  • One component with sender: 'agent' | 'user', not two components: same slots, same screen-reader need, a chat maps over messages with one field. Not role (collides with the HTML attribute) and not variant (set per message by who sent it).
  • The component owns only the frame (alignment, width, user bubble, hidden sender label). Content is children, because agent messages hold arbitrary content.
  • ChatMessageList exists so the gap between turns is a real, annotated part of the proposal and a place for list behavior later.
  • Rich agent content (markdown output) is the renderer's: fenced code maps to the PDS CodeBlock; PDS has no inline code style. One exception: ul / ol in an agent message get start padding, because the PDS reset clips the markers.
  • Type uses the semantic pds-copy-s class instead of font primitives (2026-10-19).

Full list and reasons: decisions.md.

API

ChatMessage (a div):

Prop Type Default Purpose
children ReactNode none (required) Message content: text for a user message; anything for an agent message.
className string none Additional class names.
label string none (required) Who sent it, read by screen readers before the message. Visually hidden.
sender 'agent' | 'user' 'agent' user: bubble on the right, hugs its content, max 80%. agent: no container, full width.

ChatMessageList (a div): children (the messages) and className. The consumer adds role / aria-label (for example role='log').

Variants and states: agent, user. Stories: Agent, User, Conversation.

Tokens used

  • --pds-border-radius-card
  • --pds-color-foreground-default
  • --pds-color-surface-default-secondary
  • --pds-spacing-2xs
  • --pds-spacing-l
  • --pds-spacing-s

Impact

From impact.md: no existing PDS component is reached and no --pds-* token is redefined. No other proposal imports or uses ChatMessage by code. ChatPanel composes it in its stories (ChatPanel is ready for dev; port ChatMessage first).

Reused vs net-new

  • Reused: the semantic pds-copy-s type class, the visually-hidden class, PDS tokens only. No PDS component is composed.
  • Net-new: .pds-chat-message (--agent, --user, __content) and .pds-chat-message-list; props: all of ChatMessageProps and ChatMessageListProps; custom CSS variables --chat-message-max-width (80%) and --chat-message-list-gap (--pds-spacing-l). No raw values kept. Details in custom.md.

Accessibility

  • The sender label is a visually hidden span before the content, so a screen reader announces "You" or "Zappy" first.
  • No interactive elements, so no keyboard behavior or focus styles of its own; content inside an agent message brings its own.
  • ChatMessageList has no role of its own; the screen adds one (role='log' for a streaming conversation). The live-region behavior is not designed yet.
  • The a11y panel has not been run, and light and dark were not checked by eye in a session.

Decisions and open questions

For the engineer or designer to settle:

  • Confirm the bubble radius and the 80% width against the design, and check the user fill in dark mode.
  • Should ChatMessage own the spacing between blocks inside an agent message (reference value --pds-spacing-l)? It is the renderer's today.
  • Should inline code become a PDS InlineCode component later?
  • Next for the list: scroll and live-region behavior, an avatar, agent actions (copy, regenerate, feedback), user extras (attachments, edit, timestamps).

Port notes

Would live in src/components/ (a new chat or conversation category folder; generator npm run generate:component). Not yet matching v2: the local mergeClasses stand-in (the package does not export it; use @libs/components/utils). The pds-copy-s class is !important, so a rule on the same element cannot override size or weight.

Preview

npm run storybook, story Component Proposals / AI Chat / Chat Message. Figma: https://www.figma.com/design/AMGWJbraJoi2PKWnxo29Ah/PDS-Proposals?node-id=257-43, page Chat Message (the designer's component set, not a capture).