Chat

Organism
A controlled assistant conversation with independently scrolling messages, clear author and provider identities, and an anchored multiline composer. The parent supplies the bounds; Chat fills the available height without choosing a fixed height of its own.

Playground

Every control sets a real prop of ChatProps. The JSX below updates as you go.
  • You
    Can the accent follow our brand hue?
  • Assistant
    Yes. Set one oklch hue and every ion token, glow and ring follows.
Props
plain
boolean
JSX · ChatProps
<Chat />

Variants

10 examples from src/organisms/chat/chat.md, each rendered live from its own JSX.
  • You
    Review requirement 1.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
  • You
    Review requirement 2.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
  • You
    Review requirement 3.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
  • You
    Review requirement 4.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
  • You
    Review requirement 5.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
  • You
    Review requirement 6.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
  • You
    Review requirement 7.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
  • You
    Review requirement 8.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
  • You
    Review requirement 9.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
  • You
    Review requirement 10.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
  • You
    Review requirement 11.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
  • You
    Review requirement 12.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
  • You
    Review requirement 13.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
  • You
    Review requirement 14.
  • Assistant
    Confirm the expected behavior, check mobile layouts, and record the evidence before continuing.
The composer stays here while earlier messages scroll.
Long conversation
<Chat testID="chat-long" style={{ height: 320 }} messages={Array.from({ length: 28 }, (_, index) => ({ id: index, role: index % 2 === 0 ? "user" : "assistant", content: index % 2 === 0 ? `Review requirement ${index / 2 + 1}.` : "Confirm the expected behavior, check mobile layouts, and record the evidence before continuing." }))} value="A follow-up question" onChangeText={() => {}} onSend={() => {}} footer={<Typography tiny muted>The composer stays here while earlier messages scroll.</Typography>} />
  • Planner
    OpenAI
    I am reviewing the requirements…
    Responding…
Streaming
<Chat style={{ height: 320 }} messages={[{ id: "reply", role: "assistant", author: "Planner", provider: "OpenAI", content: "I am reviewing the **requirements**…", streaming: true }]} value="Add a mobile check" onChangeText={() => {}} onSend={() => {}} pending />
  • You
    Which checks matter for this change?
Planner is reviewing the project…
Pending
<Chat style={{ height: 300 }} messages={[{ id: "request", role: "user", content: "Which checks matter for this change?" }]} value="" onChangeText={() => {}} onSend={() => {}} pending pendingIcon={<Icon sparkles accessibilityLabel="Planner" size={24} />} pendingLabel="Planner is reviewing the project…" />
Plan your next change
Describe the outcome you want.
    Anchored actions
    <Chat style={{ height: 320 }} messages={[]} value="" onChangeText={() => {}} onSend={() => {}} emptyState={<EmptyState title="Plan your next change" description="Describe the outcome you want." />} footer={<Row snug wrap><Button ghost small>AI connections</Button><Button ghost small>Review requirements</Button></Row>} />
    • Assistant
      Your answers are saved. What would you like to do next?
    Message choices
    <Stateful initial="">
    {(choice, setChoice) => (
    <ChatThread
    style={{ height: 240 }}
    messages={[{ id: "checkpoint", role: "assistant", content: choice ? `Selected: ${choice}` : "Your answers are saved. What would you like to do next?", actions: choice ? [] : [{ id: "draft", label: "Draft plan", primary: true }, { id: "continue", label: "Continue with questions" }] }]}
    onAction={(_messageId, actionId) => setChoice(actionId)}
    />
    )}
    </Stateful>
  • Planner
    OpenAI
    The reply could not be completed.
    Reply failed
    Your draft is preserved. Try again when the provider is available.
  • Delivery notice
    <ChatMessage message={{ id: "failed", role: "assistant", author: "Planner", provider: "OpenAI", content: "The reply could not be completed.", error: true, notice: "Your draft is preserved. Try again when the provider is available." }} />
    • You
      Review this plan.
    notes.txt
    Attachments
    <Chat style={{ height: 320 }} messages={[{ id: "request", role: "user", content: "Review this plan.", attachments: [{ id: "saved-plan", name: "release-plan.pdf" }] }]} value="" onChangeText={() => {}} onSend={() => {}} onAttach={() => {}} attachments={[{ id: "draft-notes", name: "notes.txt" }]} onRemoveAttachment={() => {}} onOpenAttachment={() => {}} />
    • Assistant
      Which release approach should we plan?
      1 of 2
      What should the first release include?
      Select multiple
    Selectable questions
    Move through one question at a time. This example controls the saved position and answer draft separately so navigation and answer callbacks can update together.
    <Stateful initial={{ answers: [] as { questionId: string; optionId?: string; optionIds?: string[]; customText?: string; multiple?: boolean }[], submitted: false }}>
    {(state, setState) => (
    <Stateful initial={0}>
    {(questionIndex, setQuestionIndex) => (
    <ChatThread
    style={{ height: 420 }}
    messages={[{
    id: "planning-questions",
    role: "assistant",
    content: "Which release approach should we plan?",
    questions: [{ id: "scope", title: "What should the first release include?", options: [
    { id: "focused", label: "A focused first release", description: "Ship the essential workflow, then expand from feedback." },
    { id: "complete", label: "The complete workflow", description: "Include reporting, collaboration, and administration in the first release." }
    ] }, { id: "checks", title: "Which checks should we include?", multiple: true, options: [
    { id: "desktop", label: "Desktop browser testing" },
    { id: "mobile", label: "Mobile layout and accessibility testing" }
    ] }],
    questionAnswers: state.answers,
    questionIndex,
    questionsSubmitted: state.submitted
    }]}
    onQuestionAnswerChange={(_messageId, answers) => setState({ ...state, answers })}
    onQuestionIndexChange={(_messageId, index) => setQuestionIndex(index)}
    onSubmitQuestionAnswers={(_messageId, answers) => setState({ answers, submitted: true })}
    />
    )}
    </Stateful>
    )}
    </Stateful>
    Only reviewed requirements continue to execution.
    Separate pieces
    <ChatComposer value="Review the release plan" onChangeText={() => {}} onSend={() => {}} footer={<Typography tiny muted>Only reviewed requirements continue to execution.</Typography>} />

    Release scope

    Acceptance criteria

    Behavior verified
    Requirement recorded
    Story
    Priority
    Status
    Saved scope
    Must
    Backlog
    Document Markdown
    <Markdown compact>{`# Release scope
    ## Acceptance criteria
    - [ ] Behavior verified
    - [x] Requirement recorded
    | Story | Priority | Status |
    | --- | --- | --- |
    | Saved scope | Must | Backlog |`}</Markdown>

    Anatomy

    • You
      Can the accent follow our brand hue?
    • Assistant
      Yes. Set one oklch hue and every ion token, glow and ring follows.
    1
    Thread
    2
    Assistant message
    3
    User message
    4
    Avatar
    5
    Composer
    Dimmed parts appear only in some states or variants.

    Props

    28 props on ChatProps, grouped as they are declared in source. Required props are marked *.
    Prop
    Type
    Default
    Description
    Props
    28 props
    onAction
    (messageId: string | number, actionId: string) => void
    –
    The app owns action handling, persistence, and removing resolved actions.
    onQuestionAnswerChange
    (messageId: string | number, answers: ChatQuestionAnswerData[]) => void
    –
    Update the message's controlled questionAnswers; the app owns saving the draft.
    onSubmitQuestionAnswers
    (messageId: string | number, answers: ChatQuestionAnswerData[]) => void
    –
    Submit the final complete answer set; the app owns transport and setting questionsSubmitted.
    onQuestionIndexChange
    (messageId: string | number, index: number) => void
    –
    Save the zero-based current question index; omit questionIndex for local navigation state.
    disabled
    boolean
    false
    Disable question forms while other application work is in progress.
    messages *
    readonly ChatMessageData[]
    –
    onOpenAttachment
    (attachment: ChatAttachmentData) => void
    –
    pending
    boolean
    false
    A reply is being prepared; no artificial message is added to the data.
    pendingLabel
    string
    –
    pendingIcon
    ReactNode
    –
    Application-supplied provider mark for a reply that has not started streaming.
    label
    string
    –
    emptyState
    ReactNode
    –
    plain
    boolean
    false
    testID
    string
    –
    style
    LayoutStyle
    –
    Parent composition only. Supply a bounded height, or a bounded flex parent.
    value *
    string
    –
    onChangeText *
    (value: string) => void
    –
    onSend *
    (value: string) => void
    –
    Receives the trimmed draft. The app owns clearing the draft and starting transport.
    placeholder
    string
    –
    composerLabel
    string
    –
    sendLabel
    string
    –
    sendDisabled
    boolean
    false
    Block only sending, keeping the draft, attachments, and message choices interactive.
    maxLength
    number
    –
    Optional application input limit; enforced by the native text field.
    onAttach
    () => void
    –
    Show an attachment control; the app owns its file picker and upload service.
    attachments
    readonly ChatAttachmentData[]
    –
    onRemoveAttachment
    (id: string) => void
    –
    attaching
    boolean
    false
    File selection or upload is in progress; prevents attaching or sending again.
    footer
    ReactNode
    –
    Anchored actions or help below the composer, outside the scrolling thread.

    Accessibility and keyboard

    Keyboard
    Type
    Edits the value
    Tab
    Next field
    Escape
    Clears when clearable
    Semantics
    Roles
    header
    heading
    progressbar
    text
    States
    busy
    ARIA on web
    aria-busy
    aria-hidden
    aria-label
    aria-level
    aria-live
    From the docs
    Messages remain application-owned ChatMessageData: keep each id stable while updating content during streaming. pending shows a three-dot thinking indicator until an explicitly streaming message arrives; pass pendingIcon to identify its provider. Streaming replies retain their own Responding indicator. Both animations stop when their state ends and stay still under Reduce Motion. providerIcon accepts an application-supplied provider logo beside the author; label the image or icon with the provider nam
    ChatThread follows new content while the reader is within 48 points of the bottom. Scrolling up to read older messages pauses following; returning to the bottom resumes it. Message updates, streaming text, question-height changes, and viewport resizing coalesce into a tail adjustment after layout settles. Scrolling is immediate and never animated, so it does not add motion when text streams or Reduce Motion is enabled. The overflowing thread is keyboard focusable with a visible focus frame. The native app or owni
    Set onAttach to show the accessible paperclip action. The app owns file picking and uploading, passes composer attachments, and removes a staged file through onRemoveAttachment(id). A ChatAttachmentData has an id, name, optional uploading or error flags, and optional notice text. Uploading and failed files block sending; remove a failed file or replace it after retrying. attaching also blocks attaching and sending while the app opens a picker or uploads. Saved message attachments use the same me
    Both answer callbacks and an enabled form are required for interaction. disabled, pending, and active streaming lock the current question. Use questionsDisabled for an inactive earlier batch: it shows every original question and option read-only, without navigation actions. questionsSubmitted shows saved answers with an Answers sent receipt. Multiple-choice receipts include every selected label and custom text; an answer-mode override is identified as Multiple answers selected by you. Question labels and de
    The current question is framed by the existing compact, flat selected Card: a soft theme tint and primary border distinguish its counter, answers and actions from the surrounding conversation. Saved receipts and inactive question history remain unframed. The Card is visual only, so answer controls retain their own accessible roles and focus behavior.
    Markdown reuses Chat's safe grammar and renderer for standalone documents. It preserves heading levels one through six, emphasis, inline and fenced code, quotes, ordered and unordered lists, nested indentation, static checked/unchecked task lists, horizontal rules, and pipe tables. Tables use Canvas DataTable's container-responsive stacked layout on narrow surfaces, retaining every column on iOS too. Checklists are read-only Canvas selection controls; reading a document never changes its source. HTML, links and i

    Platform skins

    The playground's props on each platform: same tree, native shapes, type and touch sizes.
    • Assistant
      Your answers are saved. What would you like to do next?
    Web
    control 10 · field 10 · card 16 · Instrument Sans
    • Assistant
      Your answers are saved. What would you like to do next?
    iOS
    capsule controls · field 8 · card 12 · SF Pro · 44 pt
    • Assistant
      Your answers are saved. What would you like to do next?
    Android
    Material 3 stadium · field 4 · card 12 · Roboto · 48 dp

    Usage

    <Chat
    style={{ height: 360 }}
    messages={[
    { id: "question", role: "user", content: "Help me plan the release." },
    { id: "answer", role: "assistant", author: "Planner", provider: "OpenAI", content: "Start with **scope** and a release checklist.\n\n1. Confirm acceptance criteria\n2. Run the affected checks\n3. Review the rollout" }
    ]}
    value=""
    onChangeText={() => {}}
    onSend={() => {}}
    />
    Messages remain application-owned ChatMessageData: keep each id stable while updating content during streaming. pending shows a three-dot thinking indicator until an explicitly streaming message arrives; pass pendingIcon to identify its provider. Streaming replies retain their own Responding indicator. Both animations stop when their state ends and stay still under Reduce Motion. providerIcon accepts an application-supplied provider logo beside the author; label the image or icon with the provider name for assistive technology. provider remains an optional supplementary text label such as the transport name. error adds a visible failure label; notice preserves delivery or fallback information. footer anchors connection, review, or other actions below the composer and outside the scrollport.
    Pass actions on any message for app-owned choices beneath its content: each has a stable id, a literal label, and optional primary. Chat, ChatThread, and ChatMessage call onAction(messageId, actionId) when chosen. Choices wrap at narrow widths and use each platform’s Button. disabled, pending, any streaming message in the thread, or an absent callback prevents activation. The app owns saving the choice, setting busy state while it is accepted, duplicate prevention, and removing resolved or historical actions. Actions do not send a composer draft or submit question answers.
    Assistant content supports paragraphs, headings, lists, quotes, fenced code, bold, emphasis, and inline code through React Native text. HTML, images, and links stay literal text and never load or execute remote content. plain renders all source punctuation literally; user messages are always plain text. This is a small safe markdown subset rather than a complete CommonMark renderer.

    Tokens

    Read from the playground render. Values follow the scheme and the ion hue.
    --bg2
    --fd
    --ft
    --ion
    --ion-fg
    --ion-soft
    --ion-tx
    --ln
    --ln2
    --sf
    --sf2
    --tx3

    Related components