Chat thread

A chat thread with grouped messages, reactions, read receipts, typing, and a composer with attachments.

pnpm dlx shadcn@latest add @uiarc/chat-thread
Live · keyboard ready

CSV export limit

Chloe Nguyen, Daniel Kim

  • Customer support or in-app messaging with read receipts and typing state.
  • Team or DM threads that need reactions and image or file attachments.
  • Comment threads on an object where replies arrive live.
  • Use log-stream for streaming system or build output.
  • Use timeline for an activity history rather than a two-way conversation.
  • Use textarea or mention-input when you only need the input box inside your own layout.

Installation

Add Chat thread with the shadcn CLI, or copy the source by hand.

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

Adds the component and its local dependencies, and installs motion, lucide-react. First time? Add the @uiarc registry to components.json, or use the full URL:

example.tsx
import { ChatThread, type ChatMessage } from "@/registry/components/chat-thread/chat-thread"; const participants = [  { id: "me", name: "Alex Rivera" },  { id: "sam", name: "Sam Lee", avatar: "/avatars/sam.jpg" },]; const messages: ChatMessage[] = [  { id: "m1", authorId: "sam", text: "Is the build green?", createdAt: "2026-09-23T09:12:00Z" },  { id: "m2", authorId: "me", text: "Yes, shipping now.", createdAt: "2026-09-23T09:13:00Z", status: "delivered" },]; export function SupportChat() {  return (    <div style={{ height: 520 }}>      <ChatThread        participants={participants}        currentUserId="me"        defaultMessages={messages}        readBy={{ sam: "m2" }}        typing={["sam"]}        onSend={draft => console.log(draft.text, draft.files)}      />    </div>  );}

API reference

5 parts. The first is the root.

ChatThread

A chat or support thread with grouped messages, day separators, reactions, read receipts, a typing indicator, and a composer. It stays pinned to the newest message and shows a pill when new messages land out of view.

PropTypeDefaultDescription
participantsRequiredChatParticipant[]–Everyone in the thread: id, name, and optional avatar URL. Initials show without an avatar.
currentUserIdRequiredstring–Id of the viewer. Their messages sit on the right in the accent color.
messagesChatMessage[]–Controlled messages, oldest first. Leave it out to let the thread keep its own list.
defaultMessagesChatMessage[]–Initial messages when uncontrolled.
typingstring[][]Participant ids currently typing. The current user is ignored.
readByRecord<string, string>–The last message each participant has read, by participant id. Their avatar sits under that message and glides as it moves.
onSend(draft: ChatDraft) => void | Promise<void>–Called with the composer's text and files. Uncontrolled threads also append the message themselves with status sent.
onReact(messageId: string, emoji: string) => void–Called when a reaction is picked or toggled. Uncontrolled threads update counts themselves.
onRetry(messageId: string) => void–Shows a Retry button under the last own message when its status is failed.
reactionsstring[]["👍", "❤️", "😂", "🎉", "👀", "🙏"]Emoji offered by the reaction picker.
placeholderstring"Message"Composer placeholder, also its accessible name.
composerbooleantrueShow the composer. Turn it off for a read-only transcript.
allowAttachmentsbooleantrueAllow files by button, paste, or drop in the composer.
acceptstring–File types the attach button offers, as for <input accept>.
groupWindownumber300000Messages from one person closer together than this, in ms, share a group. Defaults to five minutes.
localestring"en-US"Locale for times and day labels.
labelstring"Conversation"Accessible name of the message log.
classNamestring–Extra class on the root.

ChatThreadHandle

Methods on the ref.

PropTypeDefaultDescription
scrollToBottom(smooth?: boolean) => void–Scrolls to the newest message and pins the thread there. Smooth by default, instant with reduced motion.
focusComposer() => void–Focuses the composer textarea.

ChatComposer

The message box on its own: autosizing text up to six lines, attachments by button, paste, or drop, and a send button that lifts its arrow on send.

PropTypeDefaultDescription
onSendRequired(draft: ChatDraft) => void–Called with trimmed text and the attached files. The composer clears itself after.
placeholderstring"Message"Placeholder and accessible name of the textarea.
allowAttachmentsbooleantrueShow the attach button and accept pasted or dropped files, up to ten at a time.
acceptstring–File types the attach button offers.
disabledboolean–Disables the textarea and attach button.
classNamestring–Extra class on the composer.

ChatMessage

Shape of one message.

PropTypeDefaultDescription
idRequiredstring–Stable id. Also the key used by readBy and onReact.
authorIdRequiredstring–Participant id of the author.
textstring–Message body, rendered as plain text with line breaks kept.
createdAtRequiredDate | string | number–Send time. Drives grouping, day separators, and the time label.
attachmentsChatAttachment[]–Images (kind image with url) show in a gallery of up to four with a +N tile; everything else shows as a download row. Pass width and height to reserve space for a single image.
reactionsChatReaction[]–Emoji, count, and mine for reactions the viewer added.
status"sending" | "sent" | "delivered" | "read" | "failed"–Delivery state of the viewer's own messages. Shown under their latest message only.

formatBytes

Formats a byte count as B, KB, or MB, as used on file rows.

PropTypeDefaultDescription
bytesnumber0Size in bytes.
Enter
Sends the message from the composer. Ignored during IME composition.
Shift+Enter
Adds a line in the composer.
Tab
Reaches the add reaction button of each message; it shows on focus within the row.
Arrow LeftorArrow Right
Moves between emoji in an open reaction picker, wrapping at the ends.
Escape
Closes the reaction picker and returns focus to its button.
  • Messages are an ordered list with role log, aria-live polite, and aria-relevant additions, so new messages are read out.
  • Typing is announced in a separate status region, such as Sam is typing; the visual dots are aria-hidden.
  • Reaction chips are toggle buttons with aria-pressed and labels like 👍 3, including you.
  • Read receipts carry an aria-label listing who read the message; avatars are decorative.
  • The picker focuses its first emoji on open and uses a roving tabindex.
  • New messages rise in from their own side with a small scale on a smooth spring, and the list glides up by the added height instead of jumping.
  • Reader avatars share a layoutId, so a receipt travels to the newly read message.
  • Reaction chips pop in, counts roll up or down, and the reaction row and attachment tray spring open.
  • The send arrow lifts out and a new one rises in on each send; the jump to latest pill springs in from below.
  • Reduced motion turns these into short fades, stops the typing dots, and scrolls to the bottom instantly.
  • Bubbles are capped at min(78%, 34rem); below 420px the content padding tightens to 12px and bubbles may reach 84%.
  • On touch devices (hover none) the add reaction button stays visible at reduced opacity instead of waiting for hover.
  • The reaction picker opens above the bubble unless that would pass the top of the scroller, then opens below.
  • Every message renders; there is no virtualization, so page older history in chunks past a few hundred messages.
  • A ResizeObserver on the content keeps the list pinned and drives the glide; images take width and height to avoid layout shift.
  • Uncontrolled sends create object URLs for attached files and never revoke them; controlled threads should upload and pass real URLs.

Notes for AI

Give your coding assistant the Markdown reference instead of screenshots.

  • Choose it for support widgets, DMs, and team chat. Use log-stream for machine output and timeline for dated events that are not a conversation.
  • Give the parent a fixed height; the root is a flex column at height 100% with its own scroller.
  • For a server-backed thread, pass messages and append an optimistic message with status sending in onSend, then update status or set failed and handle onRetry.
  • Keep readBy in sync from your realtime source; only the furthest read message marks the viewer's latest message as Read.
  • Use ChatComposer alone when messages render elsewhere, such as an AI chat.

The full library index for assistants is at /llms.txt.