Chat Components
A compound family of three chat primitives — ChatList, ChatMessage, and TypingIndicator — for lessons that explain WebSocket protocols, debounced typing emissions, message grouping, or auto-scroll behavior. Drop in a messages[] array and the list mirrors it.
Preview
- Mira joined the chat
- MiraCan you explain what a typing indicator actually emits?
- HelperSure — most chat protocols debounce keystrokes and emit a single "typing" event after a quiet 300ms.
- That's why your dots don't flicker on every keystroke.
- MiraAh, got it. Thanks!
Helper is typing...
Customize
Options
Installation
npx shadcn@latest add https://craftbits.dev/r/chat-components.jsonUsage
import { ChatList } from "@craft-bits/core";
<ChatList
messages={[
{ id: "1", role: "user", author: "Mira", content: "Hey!", timestamp: Date.now() },
{ id: "2", role: "assistant", author: "Helper", content: "Hi there.", timestamp: Date.now() },
]}
typingUsers={["Helper"]}
/>Compose your own list by reaching for the inner pieces directly:
import { ChatMessage, TypingIndicator } from "@craft-bits/core";
<ul role="list" className="flex flex-col gap-2">
<ChatMessage message={{ id: "1", role: "user", author: "Mira", content: "Hi" }} />
<ChatMessage message={{ id: "2", role: "assistant", author: "Helper", content: "Hello" }} />
</ul>
<TypingIndicator users={["Helper"]} />Anatomy
- ChatList. A
<section role="log">with a scrollable inner<ul>. Wraps each message in<ChatMessage>, animates entrance viaAnimatePresence, and auto-scrolls the tail into view when new messages arrive. - ChatMessage. One
<li>per message.role="user"aligns right with an accent-tinted bubble;role="assistant"aligns left;role="system"renders centered with a muted pill. - TypingIndicator. Three pulsing dots followed by a "X is typing..." label. Renders nothing when the
usersarray is empty.
Understanding the component
- Role-driven layout. The author
role(user/assistant/system) picks the alignment, the bubble tone, and the live-region behavior. NoisOwnflag — the role is the ownership. - Message grouping. Consecutive messages from the same role + author collapse their avatar and author line via
groupConsecutive(defaulttrue). The bubble's top corner tightens so the cluster reads as one turn. - Avatar hue.
avatarHuelets you pin a hue; otherwise the author string is hashed into one of ten palette entries — stable across renders, no random colors. - Auto-scroll.
autoScroll(defaulttrue) callsscrollIntoViewon a tail sentinel whenever the message count or typing list changes. Underprefers-reduced-motion, the scroll is"instant". - Reduced motion. Every entrance animation, the typing dot pulse, and the auto-scroll all short-circuit to instant or skipped under
prefers-reduced-motion.
Props
ChatList
| Prop | Type | Default | Description |
|---|---|---|---|
messages | ChatMessageData[] | required | Ordered list of messages to render. |
typingUsers | string[] | [] | Authors currently composing — when non-empty, mounts a typing indicator. |
hideTimestamps | boolean | false | Hide the timestamp under every bubble. |
groupConsecutive | boolean | true | Collapse avatar + author line on follow-ups from the same role + author. |
autoScroll | boolean | true | Scroll to the tail when the message count changes. |
ariaLabel | string | "Chat messages" | Accessible label for the chat region. |
className | string | — | Merged onto the root via cn(). |
ChatMessage
| Prop | Type | Default | Description |
|---|---|---|---|
message | ChatMessageData | required | The message to render. |
grouped | boolean | false | Suppress the avatar + author line for a follow-up turn. |
hideTimestamp | boolean | false | Hide the time under the bubble. |
ChatMessageData
| Field | Type | Description |
|---|---|---|
id | string | Stable identifier — used as the React key. |
role | 'user' | 'assistant' | 'system' | Author role — drives alignment and tone. |
content | ReactNode | Body of the message. |
author | ReactNode | Optional display name. |
timestamp | number | Milliseconds since epoch. Rendered as a locale-formatted HH:MM. |
avatarHue | number | Hue (0–360) for the avatar circle. Defaults to a hash of the author string. |
TypingIndicator
| Prop | Type | Default | Description |
|---|---|---|---|
users | string[] | [] | Users currently typing. Picks the singular/dual/plural label. |
label | ReactNode | — | Override the rendered text. Bypasses the users label generator. |
Accessibility
- The list root is a
<section role="log" aria-live="polite">with adata-cb-edu="chat-list"hook. New messages are announced politely so screen readers do not interrupt mid-utterance. - Each message exposes
data-cb-role="user" | "assistant" | "system"anddata-cb-groupedso consumers can extend tone-specific styling without monkey-patching CSS. - Avatars use
aria-hiddenand a spacer placeholder keeps grouped messages indented under their leader. TypingIndicatoris arole="status"live region so the announcement reads as "Helper is typing..." once, not on every dot pulse.- Timestamps are wrapped in
<time dateTime>with the ISO timestamp so assistive tech can read the absolute time. - Every entrance animation short-circuits under
prefers-reduced-motion, and auto-scroll uses"instant"instead of"smooth".
Credits
- Extracted from:
terminal-dreams(src/components/frontend-design/sdp-chat/ui/ChatComponents.tsx). The originalPersistentChatwas a single 265-line component fused to auseChatcontext, a 6-flag step-feature gate (typing / delivery / reactions / readReceipts / encryption / sendMessage), a CSS-module styling layer, and the lesson's composer/input/keystroke-WS-debounce stats panel. craft-bits splits the rendering primitives out of the controller — no context, no step gates, no composer — leaving three slot-friendly pieces (ChatList,ChatMessage,TypingIndicator) that any chat lesson or app can compose.