CommentThread
v1.0.31Updated 20 Oct 2026Threaded discussion component — nested replies, reactions, role badges, rich-text bodies and score/sort controls for internal comment feeds.
Preview
Live · 14 stories▸Why this is recommendedcommentsthreadredditdiscussion+8
The default, out-of-the-box CommentThread.
- ✓Pulse discussion threads
- ✓Casebook ticket conversations
- ✓document review comments
- ✓community/forum pages
- ✓a plain comment log on a leave or expense request, where each note is plain text with files attached
- ✓internal notes on a record, with no voting
- ✓an in-app bug reporter where a comment carries a screenshot
- ✓image comments, where a reply can show a picture
- Nested reply tree with Reddit-style collapse rails and collapsed summary rows
- Up/down voting with optimistic local score and aria-pressed state; under Best/Top a vote re-ranks the comment at once (same live score drives the number and the order, ties oldest first, focus stays on the vote button)
- Inline reply + edit composers built on RichTextEditor; async submit with pending state
- Sort by Best / Top / New / Old (pinned comments always first)
- Pinned, [deleted] and [removed by moderator] states
- Emoji reaction chips with reacted state, counts, and an add-reaction picker
- @mention highlighting with onMentionClick (event-delegated, DOMPurify-sanitized bodies)
- Load more replies (async spinner) and Continue-thread past maxDepth (opens the next maxDepth levels in place unless onContinueThread is supplied)
- Locked-thread banner that disables all composers
- Own-comment Edit / Delete (Popconfirm) via a per-comment menu; Report for others only when onReport is supplied (no empty menu trigger is rendered)
- Focus is handed on by every action, so the thread is usable with the keyboard alone: choosing Edit or Reply lands the caret in the composer after the existing text, Save / Cancel / Send return focus to the control that opened it, confirming Delete focuses the comment it emptied, collapse and expand move to the control that replaced the one pressed, removing the last reaction moves to the add-reaction button, and an action that opens nothing (Report, a dismissed menu) leaves focus on the trigger. Reported by ace-erp-app-phase2 as fb_e14ddbdb, where Edit left the caret on the more-actions trigger and typing went nowhere
- Read-only mode when currentUser is omitted
- Business-log mode: votes={false} drops the scores and the sort control, allowReplies={false} flattens the thread, showSort re-adds a New/Old picker
- Plain-text mode (bodyFormat="text"): bodies are rendered literally as text (a body of <b>hi</b> shows those characters, never bold) with line breaks kept, and the composer is a Textarea — for an API that stores plain text and must not accept markup
- Per-comment file attachments (name + optional host-formatted size + a download handler) and an attach control on the reply composers; picked File objects reach the host via onReply. The component stores and uploads nothing, and an attachment with no handler renders as a chip instead of a dead button
- Attachment names are rendered as text with control and bidi-override characters stripped, so a file cannot inject markup or disguise its extension
- Images in a body, opt-in: allowImages adds <img> to the sanitizer allow-list (src/alt/title/width/height only — no srcset, no style, no event handlers), and allowImageSrc is the host URL allow-list every surviving image is judged by. The default policy admits relative and http(s) URLs only, so javascript:, data: and scheme-relative tracking URLs never load; the component forces loading=lazy, decoding=async and referrerpolicy=no-referrer, and gives an image with no alt an empty one
- Per-comment host slot: renderFooter(node) renders anything of your own under one comment (an attachment strip, an image the host must fetch with its own credentials, a workflow control) and is suppressed on deleted/removed comments like the body is. Every comment also carries data-slot="comment-thread-comment" and data-comment-id, so a host can target one comment without matching on its text
- i18n: every built-in string overridable via the labels prop (CommentThreadLabels); relative/absolute timestamps and all counts follow LocaleProvider (Intl-driven, Buddhist-calendar aware, no bundled locale data)
- ✕Real-time chat (use Chat)
- ✕Flat activity feeds (use Timeline)
Install
Ask your agent, or call the MCP tool directly. The resolved bundle is written into your project.
Usage
Tick variants in the left menu — each adds a tab with that variant’s real story source, the same code the preview above renders.
Props
editorLabelsStrings for the RichTextEditor the composer renders under bodyFormat='html' — its toolbar, block-type menu and link popover. A separate bag because they are another component's words; without it a thread translated to the last key still showed an English toolbar (fb_75d1c537).
—commentsControlled tree: id, author, body (HTML, or plain text under bodyFormat="text"), createdAt, score (optional), children, pinned, deleted, reactions, attachments, moreCount
—defaultCommentsUncontrolled mode: the thread manages its own tree; all mutations work with zero wiring
—votesUp/down voting (default true). false removes the vote buttons, the score readout and the sort control — the plain business comment log. Best/Top then degrade to oldest-first, since there is no score to rank by
—allowRepliesInline replies (default true). false gives a FLAT log: no Reply buttons and no reply composers below the root one
—showSortShow the sort picker. Defaults to the value of votes; forced on with votes off, only New / Old are offered
—bodyFormatHow CommentNode.body is interpreted (default html, DOMPurify-sanitized, RichTextEditor composer). "text" renders the body LITERALLY as text (never parsed as markup, line breaks preserved) and composes with a Textarea
—allowAttachmentsShow an attach control on the reply composers (default false). Picked files arrive as the third argument of onReply; this component uploads and stores nothing
—acceptAttachmentsaccept for the composer file input, e.g. "application/pdf,image/png,image/jpeg". A browser hint only — the host must re-validate type and size on the server
—maxAttachmentsCap on files per composer submission; the attach control disables at the limit
—onDownloadAttachmentFallback download handler for attachments with no onDownload of their own. With neither, an attachment renders as a plain chip (no dead button)
—allowImagesAllow <img> in an HTML body (default false). Off by default because a body is HTML written by someone else, and an image tag makes the reader browser fetch a URL that author chose. No effect under bodyFormat="text"
—allowImageSrcThe host URL allow-list, checked on every surviving <img> with the src EXACTLY as the author wrote it (never resolved against the page). Defaults to isSafeCommentImageSrc: relative and absolute http(s) only. Replace it to pin an origin, or to permit an object URL the host created. Only consulted with allowImages on
—renderFooterRender host content under one comment — an attachment strip, a workflow control, an image the host has to fetch with its own credentials. Runs for every comment (return null to add nothing) and sits below the body and the built-in attachment list
—reactionPresetEmoji offered by the add-reaction picker
—currentUserEnables composers and own-comment edit/delete; omit for read-only
—sortControlled sort (defaultSort for uncontrolled). best and top both rank by live net score (including the vote just cast by the reader), ties oldest first; pinned always first
—lockedLocked banner; disables replying
—maxDepthNesting depth before "Continue this thread" (default 6)
—onContinueThread"Continue this thread" handler (e.g. route to a permalink). Omit to open the next maxDepth levels in place
—onVoteVote changes (0 = retracted)
—onReplyNew comment; parentId null = top level. body is HTML, or plain text under bodyFormat="text". attachments carries the files picked in the composer (only with allowAttachments)
—onEditSave an edited comment. Controlled mode: the Edit menu item renders only when supplied
—onDeleteConfirmed via Popconfirm. Controlled mode: the Delete menu item renders only when supplied
—onLoadMoreFetch the moreCount replies of a node
—onReactToggle a reaction chip
—onMentionClickClick/Enter on an @mention
—onShareShare action; defaults to copying the #comment-<id> link to the clipboard
—onReportReport a comment by someone else. The Report menu item (and, when it is the only action, the More-actions trigger) renders only when supplied
—labelsOverride any built-in UI string (i18n); merged over English defaults (DEFAULT_COMMENT_THREAD_LABELS)
—