- Accordion
- Action Bar
- Action Toast
- Alert Banner
- Alert Dialog
- Alert
- Alphabet Index
- Amount Field
- Aspect Ratio
- Attachment
- Availability List
- Avatar
- Badge
- Bank Card
- Breadcrumb
- Bubble
- Button Group
- Button
- Calendar
- Camera Frame
- Card Design Picker
- Card Details
- Card Peek
- Card Stack
- Card
- Carousel
- Chart
- Chat Thread
- Checkbox
- Chip Group
- Choice Tile
- Code Block
- Code Dots
- Collapsible
- Combobox
- Command
- Context Menu
- Copy Field
- Countdown
- Country Select
- Data Table
- Date Field
- Date Picker
- Date Range Picker
- Dialog
- Direction
- Document Paper
- Drawer
- Dropdown Menu
- Empty
- Entity Avatar
- Field
- File Trigger
- File Upload Item
- Hover Card
- Input Group
- Input OTP
- Input
- Item
- Kbd
- Key Value
- Keypad
- Label
- Live Region
- Marker
- Message Scroller
- Message
- Money Amount
- Native Select
- Number Field
- Pagination
- Percent Split Editor
- Phone Field
- Popover
- Progress
- QR Code
- Questionnaire
- Radio Group
- Range Histogram
- Rating
- Resizable
- Scroll Area
- Search Field
- Select
- Separator
- Share Bar
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Sortable List
- Spinner
- Status Timeline
- Step Header
- Steps List
- Swipe Row
- Switch
- Table
- Tabs
- Text Field
- Textarea
- Tier Ladder
- Toggle Group
- Toggle
- Token Search Field
- Tooltip
- Trace
- Transaction Row
- Usage Meter
Jordan requests
$64.00$64.00Concert 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
ChatThreadComposerChatThreadQuickReplies 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.
"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".
"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.
You sent
$48.00$48.00Dinner at Coffee Point
Completed · 23:44
Jordan requested
$12.50$12.50Taxi 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$25.00Groceries
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.
"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.
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | "Messages" | Names the conversation. |
className | string | - | Additional classes to apply to the thread. |
...props | React.ComponentProps<"div"> | - | Props spread to the div with role="log". |
ChatThreadEvent#
A date or system line between messages, built on Marker.
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | The text, e.g. "Today". |
className | string | - | Additional classes to apply to the event. |
...props | React.ComponentProps<"div"> | - | Props spread to the marker. |
ChatThreadMessage#
One text message in a bubble, built from Message and Bubble.
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "incoming" | "outgoing" | "incoming" | "outgoing" sits on the end side on primary. |
avatar | React.ReactNode | - | Avatar beside the message, e.g. in a group chat. |
author | React.ReactNode | - | Name above the bubble. |
meta | React.ReactNode | - | Time and delivery status under the bubble. |
senderLabel | string | "You" for outgoing messages | Read before the message by screen readers. |
children | React.ReactNode | - | The message text. |
className | string | - | Additional classes to apply to the message. |
...props | Omit<React.ComponentProps<"div">, "children"> | - | Props spread to the message root. |
ChatThreadCard#
A card for a payment, request or quote.
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
className | string | - | Additional classes to apply to the card. |
...props | React.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.
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | - | The amount. |
size | React.ComponentProps<typeof MoneyAmount>["size"] | "xl" | The amount size. |
...props | React.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.
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | "Suggested replies" | Names the group. |
className | string | - | Additional classes to apply to the group. |
...props | React.ComponentProps<"div"> | - | Props spread to the div. |
ChatThreadQuickReply#
One suggested reply. Renders a Button and accepts all of its props.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | React.ComponentProps<typeof Button>["variant"] | "secondary" | The button variant. |
size | React.ComponentProps<typeof Button>["size"] | "sm" | The button size. |
...props | React.ComponentProps<typeof Button> | - | Props spread to the Button. |
ChatThreadTyping#
Three dots in an incoming bubble.
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "Typing" | What screen readers hear, e.g. "Sam is typing". |
avatar | React.ReactNode | - | Avatar beside the bubble. |
className | string | - | Additional classes to apply to the indicator. |
...props | Omit<React.ComponentProps<"div">, "children"> | - | Props spread to the indicator root. |
ChatThreadComposer#
The message field with a send button. Renders a <form>.
| Prop | Type | Default | Description |
|---|---|---|---|
onSend | (text: string) => void | - | Called with the trimmed text. |
value | string | - | The controlled text. The field does not clear itself when set. |
defaultValue | string | "" | The initial text when uncontrolled. |
onChange | (value: string) => void | - | Called when the text changes. |
placeholder | string | "Message" | Placeholder text. |
label | string | placeholder | Accessible name of the field. |
sendLabel | string | "Send message" | Accessible name of the send button. |
isDisabled | boolean | false | Disables the field and the send button. |
start | React.ReactNode | - | Controls before the field, e.g. an attach button. |
enterToSend | boolean | true | Enter sends and Shift+Enter adds a line. Turn off on touch screens. |
inputRef | React.Ref<HTMLTextAreaElement> | - | Ref to the text field. |
sendVariant | "default" | "secondary" | "default" | "secondary" when the screen already has a primary action. |
className | string | - | Additional classes to apply to the form. |
...props | Omit<React.ComponentProps<"form">, "onSubmit" | "onChange" | "children"> | - | Props spread to the form. |
On This Page
InstallationUsageCompositionFeaturesMessagesGroupCardsCard errorQuick repliesComposerAccessibilityName the threadSay who sent each messageTyping and composer labelsKeep focus in placeAPI ReferenceChatThreadChatThreadEventChatThreadMessageChatThreadCardChatThreadCardLabelChatThreadCardAmountChatThreadCardDescriptionChatThreadCardActionsChatThreadCardErrorChatThreadCardFooterChatThreadQuickRepliesChatThreadQuickReplyChatThreadTypingChatThreadComposer