Chat thread
A chat thread with grouped messages, reactions, read receipts, typing, and a composer with attachments.
datanew
Preview
Chat thread preview
Live · keyboard ready
When to use
- 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.
When not to use
- 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.
shadcn CLI
Also installs (1)
- manicat-foundation
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "chat-thread",
"type": "registry:ui",
"title": "Chat thread",
"description": "A chat thread with grouped messages, reactions, read receipts, typing, and a composer with attachments.",
"categories": [
"data"
],
"dependencies": [
"lucide-react",
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json"
],
"files": [
{
"path": "registry/components/chat-thread/chat-thread.tsx",
"type": "registry:component",
"target": "@components/manicat/chat-thread/chat-thread.tsx"
},
{
"path": "registry/components/chat-thread/chat-thread.module.css",
"type": "registry:component",
"target": "@components/manicat/chat-thread/chat-thread.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/chat-thread",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/chat-thread",
"markdown": "https://uiarc.dev/components/chat-thread/markdown",
"tags": [
"data",
"new"
]
}
}Usage
Install the component, then copy this into your app. Change the content and the props to fit your screen.
example.tsxtsx
"use client";
import { ChatThread } from "@/components/manicat/chat-thread/chat-thread";
import type { ChatMessage, ChatParticipant } from "@/components/manicat/chat-thread/chat-thread";
const chatParticipants: ChatParticipant[] = [
{ id: "ada", name: "Ada" },
{ id: "eli", name: "Elia" },
{ id: "me", name: "You" },
];
const HOUR = 3_600_000;
const threadTime = Date.UTC(2026, 8, 9, 9);
const threadMessages: ChatMessage[] = [
{ id: "m1", authorId: "eli", text: "Do you think the slot-text reels are too slow?", createdAt: threadTime - 3 * HOUR },
{
id: "m2",
authorId: "ada",
text: "I timed them at 0.9s. Feels right for a price change.",
createdAt: threadTime - 2.6 * HOUR,
reactions: [{ emoji: "👍", count: 2, mine: true }],
},
{ id: "m3", authorId: "me", text: "Can we make the last reel land on the new digit?", createdAt: threadTime - 2.2 * HOUR, status: "sent" },
{ id: "m4", authorId: "ada", text: "Already matched. End alignment grows on the left for 999 → 1,000.", createdAt: threadTime - 2 * HOUR },
{ id: "m5", authorId: "eli", text: "Let's ship it and watch the demos.", createdAt: threadTime - HOUR },
];
export function ChatThreadExample() {
return (
<ChatThread
participants={chatParticipants}
currentUserId="me"
defaultMessages={threadMessages}
readBy={{ ada: "m4", eli: "m5" }}
placeholder="Reply in the thread…"
reactions={["👍", "❤️", "🚀"]}
label="Design feedback"
/>
);
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Design feedback thread A chat with reactions, read receipts and a composer that appends your messages.
Design feedback thread
A chat with reactions, read receipts and a composer that appends your messages. preview
Live · keyboard ready
design-feedback-thread-a-chat-with-reactions-read-receipts-and-a-composer-that-appends-your-messages-.tsxtsx
"use client";
import { ChatThread } from "@/components/manicat/chat-thread/chat-thread";
import type { ChatMessage, ChatParticipant } from "@/components/manicat/chat-thread/chat-thread";
const participants: ChatParticipant[] = [
{ id: "ada", name: "Ada" },
{ id: "eli", name: "Elia" },
{ id: "me", name: "You" },
];
const HOUR = 3_600_000;
const start = Date.UTC(2026, 8, 9, 9);
const messages: ChatMessage[] = [
{ id: "m1", authorId: "eli", text: "Are the slot-text reels too slow?", createdAt: start - 3 * HOUR },
{
id: "m2",
authorId: "ada",
text: "I timed them at 0.9s. Feels right for a price change.",
createdAt: start - 2.6 * HOUR,
reactions: [{ emoji: "👍", count: 2, mine: true }],
},
{ id: "m3", authorId: "me", text: "Can the last reel land on the new digit?", createdAt: start - 2.2 * HOUR, status: "sent" },
{ id: "m4", authorId: "ada", text: "Already matched.", createdAt: start - 2 * HOUR },
];
export function ChatThreadFeedbackExample() {
return (
<div className="w-full max-w-md">
<ChatThread
participants={participants}
currentUserId="me"
defaultMessages={messages}
readBy={{ ada: "m4", eli: "m3" }}
placeholder="Reply in the thread…"
reactions={["👍", "❤️", "🚀"]}
label="Design feedback"
/>
</div>
);
}
Keyboard interactions
| Keys | Action |
|---|---|
| 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 Left / Arrow Right | Moves between emoji in an open reaction picker, wrapping at the ends. |
| Escape | Closes the reaction picker and returns focus to its button. |
Accessibility
- 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.
Motion
- 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.
Responsive behavior
- 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.
Performance
- 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
- 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.