Threaded discussion on a record — editorial notes on a post, CRM notes on a deal, feedback on a candidate, a partner ticket. One reply level, @people and #record mentions with a picker, attachments, reactions, edit and delete, resolve and collapse, and optimistic sending / failed states.
Daniel Brooks,
Ayşe Kaya (You),
Mehmet Yılmaz,
· editedJane Cooper,
Installation
The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";
Usage
import { CommentThread } from "@vamos-ui/react";
<CommentThread
comments={comments}
currentUserId={me.id}
currentUser={me}
mentionSources={[{ search: (q) => api.people(q) }]}
onSubmit={({ body, parentId, mentions }) => api.comment(dealId, { body, parentId })}
onResolve={(id, resolved) => api.resolve(id, resolved)}
/>
// Stored bodies keep mentions as tokens:
// "@[Jane Cooper](user:42) can you check line 3?"Keyboard: the list is an ARIA feed — PageDown / PageUp move between comments. Reply moves focus into the reply box and back after sending.
The mention picker follows the combobox pattern on the textarea (aria-autocomplete, aria-controls, aria-activedescendant) and announces the number of suggestions politely.
Examples
@people and #records
One source per trigger. ↑/↓ move, Enter or Tab insert, Esc closes; the submitted body holds @[Name](kind:id) tokens.
Try “@ilk” (Turkish case folding) or “#north”, then press ⌘/Ctrl+Enter.
Files in comments (HR)
allowAttachments adds a paperclip; files arrive in onSubmit and render as chips.
Can Demir,
Sending and failed (PRM)
Add the comment with status "sending" right away; on error set "failed" and handle Retry / Discard.
Kadıköy Roasters,
Ayşe Kaya (You),
Not sent
Resolve, collapse and deleted (CMS)
Resolved threads collapse their replies behind "Show"; a deleted comment with replies leaves a placeholder.
Meltem Öz,
Resolved by İlker ŞahinMeltem Öz,
Side panel with sticky composer
layout="compact" and composer="sticky-bottom" inside a scrolling panel.
API
CommentThread
| Prop | Type | Default |
|---|---|---|
commentsAll comments; replies point to their top-level comment with parentId. | CommentData[] | — |
currentUserId / currentUserOwn comments get Edit and Delete; the avatar sits beside the composer. | string / CommentAuthor | — |
onSubmitPost a comment or reply. Without it (or with readOnly) there is no composer. | ({ body, parentId?, attachments, mentions }) => void | Promise<void> | — |
onEdit / onDeleteInline edit (Esc cancels) and delete (confirm with an undo toast in your handler). | (id, body) => … / (id) => … | — |
onResolveResolve / reopen top-level comments. | (id, resolved) => void | Promise<void> | — |
onRetry / onDiscardActions on failed comments. | (comment) => void | — |
onReact / reactionsReaction toggles and the picker; default set is icon-based (like, agree, love, question, seen). | (id, key, on) => void / ReactionOption[] | — |
mentionSources{ search(q), trigger?: "@" | "#" } — async, locale-folded ranking. | MentionSource[] | — |
renderMentionReplace the default chip (e.g. link a #record). | (mention) => ReactNode | — |
getLinkShows "Copy link". | (comment) => string | — |
allowAttachmentsPaperclip in the composer. | boolean | false |
onPreviewAttachmentOpen an attached file. | (file) => void | — |
orderTop-level order; replies are always oldest first. | "oldest-first" | "newest-first" | "oldest-first" |
layoutFull or side-panel density. | "thread" | "compact" | "thread" |
composerWhere the new-comment box sits. | "inline" | "sticky-bottom" | "top" | "inline" |
readOnlyNo composer or actions. | boolean | false |
nowPin relative times (docs, tests). | Date | string | number | — |
labelsAll built-in strings (empty, placeholder, reply, resolve, deleted, sending, failed, …). | Partial<CommentThreadLabels> | — |
CommentData
| Prop | Type | Default |
|---|---|---|
id / author / body / createdAtBody is the stored form with @[Name](kind:id) tokens. | string / CommentAuthor / string / string | — |
parentIdReply to this top-level comment. | string | — |
editedAtShows "edited" with the time as a tooltip. | string | — |
attachmentsRendered as chips. | AttachmentData[] | — |
resolved / resolvedByCollapsed state and who resolved it. | boolean / string | — |
statusOptimistic states. | "sending" | "failed" | — |
deletedPlaceholder when it has replies, hidden otherwise. | boolean | — |
reactionsReaction counts. | { key; count; reacted?; users? }[] | — |
Parts and helpers
| Prop | Type | Default |
|---|---|---|
CommentComposerThe box on its own (⌘/Ctrl+Enter sends). | onSubmit, mentionSources, allowAttachments, defaultValue, onCancel, variant | — |
CommentOne <article> labelled by author and time. | comment, currentUserId, isReply, handlers | — |
CommentBody / MentionChipRender a stored body; one mention token. | body / mention | — |
parseMentions(body)Plain text plus ids and offsets. | { text, mentions[] } | — |
serializeMention(item)"@[Jane Cooper](user:42)". | string | — |