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. Notrole(collides with the HTML attribute) and notvariant(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. ChatMessageListexists 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/olin an agent message get start padding, because the PDS reset clips the markers. - Type uses the semantic
pds-copy-sclass 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-stype class, thevisually-hiddenclass, PDS tokens only. No PDS component is composed. - Net-new:
.pds-chat-message(--agent,--user,__content) and.pds-chat-message-list; props: all ofChatMessagePropsandChatMessageListProps; custom CSS variables--chat-message-max-width(80%) and--chat-message-list-gap(--pds-spacing-l). No raw values kept. Details incustom.md.
Accessibility
- The sender
labelis a visually hiddenspanbefore 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.
ChatMessageListhas 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
ChatMessageown 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
InlineCodecomponent 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).