Dots

Chat Thread

The parts of a conversation, from message bubbles and payment cards to quick replies, a typing indicator and a composer.

Today
Jordan: Dinner was great. I booked the concert tickets too.

Jordan requests

$64.00

Concert tickets

14:10 · Expires in 7 days

"use client"

import * as React from "react"

The ChatThread component lays out a conversation between people: text messages, date and system events, inline cards for payments and requests, suggested replies, a typing indicator and the message field. Use it for chats where money moves inside the conversation, such as splitting a bill or answering a request. For a single bubble outside a thread, use Message and Bubble directly.

Installation

pnpm dlx shadcn@latest add @dots/chat-thread

Usage

import {
  ChatThread,
  ChatThreadCard,
  ChatThreadCardAmount,
  ChatThreadCardFooter,
  ChatThreadCardLabel,
  ChatThreadComposer,
  ChatThreadEvent,
  ChatThreadMessage,
} from "@/components/ui/chat-thread"
<ChatThread aria-label="Messages with Jordan Lee">
  <ChatThreadEvent>Today</ChatThreadEvent>
  <ChatThreadMessage senderLabel="Jordan">Can you cover your half?</ChatThreadMessage>
  <ChatThreadCard direction="outgoing">
    <ChatThreadCardLabel>You sent</ChatThreadCardLabel>
    <ChatThreadCardAmount value={48} />
    <ChatThreadCardFooter>Completed · 23:44</ChatThreadCardFooter>
  </ChatThreadCard>
</ChatThread>
<ChatThreadComposer onSend={(text) => send(text)} />

Composition

Use the following composition to build a chat thread:

ChatThread
├── ChatThreadEvent
├── ChatThreadMessage
├── ChatThreadCard
│   ├── ChatThreadCardLabel
│   ├── ChatThreadCardAmount
│   ├── ChatThreadCardDescription
│   ├── ChatThreadCardError
│   ├── ChatThreadCardActions
│   └── ChatThreadCardFooter
└── ChatThreadTyping
ChatThreadQuickReplies
└── ChatThreadQuickReply
ChatThreadComposer

ChatThreadQuickReplies and ChatThreadComposer sit outside the ChatThread log, under it.

Features

  • One polite live region (role="log") for the whole thread, so new messages are read out without each part being its own region
  • Incoming and outgoing messages with an optional avatar, author name and meta line
  • Cards for payments, requests and quotes, with a struck-through amount when status="inactive"
  • An inline error slot for a failed payment that takes no space while empty
  • Suggested replies as a labelled group of buttons
  • A typing indicator announced once as part of the log
  • A composer where Enter sends and Shift+Enter adds a line, IME-safe, with send disabled while the field is empty

Messages

ChatThreadMessage defaults to direction="incoming" on the start side. Set direction="outgoing" for the current user's messages and use meta for the time and delivery status. ChatThreadEvent marks a date or a system line between messages.

Yesterday
Sam: Are we still splitting the rent this month?
You: Yes, I will send my half on Friday.
21:08 · Read
Today
Sam: Got it, thanks.
09:15
"use client"

import {

Group

In a group chat, pass avatar and author so people can tell senders apart. The author is read by screen readers too, so leave out senderLabel on these messages. ChatThreadTyping takes the same avatar and a label such as "Riley is typing".

Riley created the group
R
Riley
Flights are booked for the 14th.
C
Casey
I will split the hotel once the price is final.
You: Count me in.
18:03 · Read
R
Riley is typing
"use client"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"

Cards

Use ChatThreadCard for anything richer than text: money sent, a request, a quote. Set status="inactive" for a request that was declined or expired; the amount is struck through, and the footer says why.

Nov 9

You sent

$48.00

Dinner at Coffee Point

Completed · 23:44

Jordan requested

$12.50

Taxi home

Expired · Nov 2

"use client"

import {

Card error

Keep ChatThreadCardError mounted with role="alert" and render the error inside it only when the payment fails. The error is then announced, and the slot takes no space while it is empty. In this demo the first attempt fails; once the retry succeeds the actions are removed, so focus moves to the footer with the new status.

Alex requests

$25.00

Groceries

10:02 · Expires in 3 days

"use client"

import * as React from "react"
<ChatThreadCardError role="alert">
  {failed ? (
    <AlertBanner status="destructive" size="compact">
      <AlertBannerContent>
        <AlertBannerTitle>Payment failed. Try again.</AlertBannerTitle>
      </AlertBannerContent>
    </AlertBanner>
  ) : null}
</ChatThreadCardError>

Quick replies

Offer ChatThreadQuickReplies under the last incoming message when the answer is one of a few. Remove them once the person answers, and move focus, for example to the thread with tabIndex={-1}, since the pressed reply goes away with them.

Support: Pending payments usually settle within 3 working days. Is it still pending now?
"use client"

import * as React from "react"

Composer

ChatThreadComposer calls onSend with the trimmed text and clears the field unless value is controlled. Put an attach or amount button in start. When the screen already has a primary action, such as Pay on an open request, set sendVariant="secondary" to keep one primary.

Nothing sent yet

"use client"

import * as React from "react"

On touch screens, set enterToSend={false}: the on-screen keyboard has no Shift+Enter, so Enter adds a line and only the send button sends.

Accessibility

ChatThread is a role="log" region, so assistive tech reads new items politely as they are added. Messages, cards and the typing indicator are not live regions of their own.

Name the thread

ChatThread defaults to aria-label="Messages". Name the conversation instead, for example "Messages with Jordan Lee".

Say who sent each message

Bubbles do not show the sender's name. Outgoing messages are read as "You:" by default; pass senderLabel on incoming messages so each one starts with the sender's name. You only need it when no author is shown: a visible author is already read, so passing both makes screen readers say the name twice.

<ChatThreadMessage senderLabel="Jordan">Can you cover your half?</ChatThreadMessage>

Typing and composer labels

Give ChatThreadTyping a label with the person's name. The composer field is named by label, or by placeholder when there is no label, and the send button by sendLabel. Label any icon-only button you put in start.

Keep focus in place

When Pay or Decline removes the card's actions, move focus somewhere useful, such as the composer through inputRef, so it does not fall back to the page.

API Reference

ChatThread

The conversation column and its live region.

PropTypeDefaultDescription
aria-labelstring"Messages"Names the conversation.
classNamestring-Additional classes to apply to the thread.
...propsReact.ComponentProps<"div">-Props spread to the div with role="log".

ChatThreadEvent

A date or system line between messages, built on Marker.

PropTypeDefaultDescription
childrenReact.ReactNode-The text, e.g. "Today".
classNamestring-Additional classes to apply to the event.
...propsReact.ComponentProps<"div">-Props spread to the marker.

ChatThreadMessage

One text message in a bubble, built from Message and Bubble.

PropTypeDefaultDescription
direction"incoming" | "outgoing""incoming""outgoing" sits on the end side on primary.
avatarReact.ReactNode-Avatar beside the message, e.g. in a group chat.
authorReact.ReactNode-Name above the bubble.
metaReact.ReactNode-Time and delivery status under the bubble.
senderLabelstring"You" for outgoing messagesRead before the message by screen readers.
childrenReact.ReactNode-The message text.
classNamestring-Additional classes to apply to the message.
...propsOmit<React.ComponentProps<"div">, "children">-Props spread to the message root.

ChatThreadCard

A card for a payment, request or quote.

PropTypeDefaultDescription
direction"incoming" | "outgoing""incoming""outgoing" sits on the end side on primary.
status"active" | "inactive""active""inactive" mutes the card and strikes through the amount.
classNamestring-Additional classes to apply to the card.
...propsReact.ComponentProps<"div">-Props spread to the card element.

ChatThreadCardLabel

What the card is, e.g. "You sent" or "Jordan requests". Renders a <p> and accepts its props.

ChatThreadCardAmount

The amount on the card. Renders a MoneyAmount and accepts all of its props.

PropTypeDefaultDescription
valuenumber-The amount.
sizeReact.ComponentProps<typeof MoneyAmount>["size"]"xl"The amount size.
...propsReact.ComponentProps<typeof MoneyAmount>-Props spread to MoneyAmount.

ChatThreadCardDescription

What the money is for, e.g. "Concert tickets". Renders a <p> and accepts its props.

ChatThreadCardActions

Buttons for the card, e.g. Pay and Decline. Each child grows to fill the row. Renders a <div> and accepts its props.

ChatThreadCardError

A slot for an inline error. Not a live region by itself: add role="alert" and render the error inside only when it happens. Renders a <div> and accepts its props.

ChatThreadCardFooter

Status and time, e.g. "Completed · 23:44". Renders a <p> and accepts its props.

ChatThreadQuickReplies

A labelled group of suggested replies.

PropTypeDefaultDescription
aria-labelstring"Suggested replies"Names the group.
classNamestring-Additional classes to apply to the group.
...propsReact.ComponentProps<"div">-Props spread to the div.

ChatThreadQuickReply

One suggested reply. Renders a Button and accepts all of its props.

PropTypeDefaultDescription
variantReact.ComponentProps<typeof Button>["variant"]"secondary"The button variant.
sizeReact.ComponentProps<typeof Button>["size"]"sm"The button size.
...propsReact.ComponentProps<typeof Button>-Props spread to the Button.

ChatThreadTyping

Three dots in an incoming bubble.

PropTypeDefaultDescription
labelstring"Typing"What screen readers hear, e.g. "Sam is typing".
avatarReact.ReactNode-Avatar beside the bubble.
classNamestring-Additional classes to apply to the indicator.
...propsOmit<React.ComponentProps<"div">, "children">-Props spread to the indicator root.

ChatThreadComposer

The message field with a send button. Renders a <form>.

PropTypeDefaultDescription
onSend(text: string) => void-Called with the trimmed text.
valuestring-The controlled text. The field does not clear itself when set.
defaultValuestring""The initial text when uncontrolled.
onChange(value: string) => void-Called when the text changes.
placeholderstring"Message"Placeholder text.
labelstringplaceholderAccessible name of the field.
sendLabelstring"Send message"Accessible name of the send button.
isDisabledbooleanfalseDisables the field and the send button.
startReact.ReactNode-Controls before the field, e.g. an attach button.
enterToSendbooleantrueEnter sends and Shift+Enter adds a line. Turn off on touch screens.
inputRefReact.Ref<HTMLTextAreaElement>-Ref to the text field.
sendVariant"default" | "secondary""default""secondary" when the screen already has a primary action.
classNamestring-Additional classes to apply to the form.
...propsOmit<React.ComponentProps<"form">, "onSubmit" | "onChange" | "children">-Props spread to the form.