Comment Thread
Preview: Inline
Discussion thread: anchored quote, comments with reactions, reply box, resolve.
Import
import { CommentThread } from '@marbleui/ui'Props
<CommentThread>
| Prop | Type | Default | Description |
|---|---|---|---|
| comments *required | Comment[] | — | |
| anchor | string | — | Quoted text the thread is anchored to (inline document comments). |
| className | string | — | |
| currentUser | string | 'You' | |
| defaultResolved | boolean | false | |
| onReact | (commentId: string, emoji: string) => void | — | |
| onReply | (text: string) => void | — | |
| onResolvedChange | (resolved: boolean) => void | — | Shows the Resolve/Reopen button. |
| resolved | boolean | — | Resolved threads collapse to one line; changing it later collapses or expands the thread to match. |
Usage notes
Discussion attached to a spot in a document or design: an optional quoted anchor, comments with emoji reactions, a reply box, and Resolve/Reopen. For a reaction row under a single message use ReactionBar.
<CommentThread
anchor="The drawer opens at full height on small screens"
comments={comments}
onReply={(text) => addComment(threadId, text)}
onReact={(commentId, emoji) => toggleReaction(commentId, emoji)}
resolved={resolved}
onResolvedChange={setResolved}
/>
- Fully controlled: the thread never changes
commentsitself. Append the reply inonReply, and update counts andmineinonReact. - Parts appear only with their callback: no
onReply, no reply box; noonResolvedChange, no Resolve button; noonReact, no quick reactions, and existing reaction chips are shown as plain text rather than buttons. resolvedis controlled, or usedefaultResolvedwithonResolvedChange. A resolved thread collapses to a “show” row and hides the reply box, and it collapses or expands again wheneverresolvedchanges. “show” moves focus to the comment list.- ⌘/Ctrl+Enter sends a reply.
timeis displayed as given (“2h ago”).currentUseronly names the reply field (“Reply as …”).
Example source · show· hide
import { useState } from 'react'
import { CommentThread, type Comment } from './comment-thread'
const initial: Comment[] = [
{ id: '1', author: 'Maya Chen', time: '2h ago', body: 'Should the drawer snap to 40% on mobile? It covers the map completely right now.', reactions: [{ emoji: '👍', count: 2 }] },
{ id: '2', author: 'Charles Webb', time: '1h ago', body: 'Agreed. 40% and 90% detents, with the handle always visible.', edited: true },
]
function Demo() {
const [comments, setComments] = useState(initial)
const [resolved, setResolved] = useState(false)
return (
<CommentThread
className="w-96"
anchor="The drawer opens at full height on small screens"
comments={comments}
resolved={resolved}
onResolvedChange={setResolved}
onReply={(text) => setComments((c) => [...c, { id: String(Date.now()), author: 'You', time: 'now', body: text }])}
onReact={(id, emoji) =>
setComments((cs) =>
cs.map((c) => {
if (c.id !== id) return c
const existing = c.reactions?.find((r) => r.emoji === emoji)
const reactions = existing
? c.reactions!.map((r) => (r.emoji === emoji ? { ...r, mine: !r.mine, count: r.count + (r.mine ? -1 : 1) } : r)).filter((r) => r.count > 0)
: [...(c.reactions ?? []), { emoji, count: 1, mine: true }]
return { ...c, reactions }
}),
)
}
/>
)
}
export const styles = { Inline: Demo, Resolved: () => <CommentThread className="w-96" comments={initial} resolved onResolvedChange={() => {}} /> }Source: packages/ui/src/comment-thread/comment-thread.tsx
Catalog
- Category
- editor
- Priority
- Pro
- Primitive
- composed
- React Aria
- GridList + TextField