Chat thread

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

datanew
Preview
Chat thread preview
  1. Elia

    Do you think the slot-text reels are too slow?
  2. Ada

    I timed them at 0.9s. Feels right for a price change.
  3. Can we make the last reel land on the new digit?
    Sent
  4. Ada

    Already matched. End alignment grows on the left for 999 → 1,000.
  5. Elia

    Let's ship it and watch the demos.

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
  1. Elia

    Are the slot-text reels too slow?
  2. Ada

    I timed them at 0.9s. Feels right for a price change.
  3. Can the last reel land on the new digit?
    Read
  4. Ada

    Already matched.

Keyboard interactions

KeysAction
EnterSends the message from the composer. Ignored during IME composition.
Shift+EnterAdds a line in the composer.
TabReaches the add reaction button of each message; it shows on focus within the row.
Arrow Left / Arrow RightMoves between emoji in an open reaction picker, wrapping at the ends.
EscapeCloses 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.