Comments
Configure Editor comments, thread storage, and Markdown range persistence.
Overview
Editor uses BlockNote comments for range-based document comments. Cubitt
renders the sidebar and thread cards, while BlockNote owns the comment mark
model, thread store contract, and comment body format. Comments work in both
blocknote and markdown content modes.
Cubitt owns the editor integration and range marker synchronization. Consumers own thread storage, user resolution, permissions, persistence, and workflow state.
<Editor.Root
comments={{
currentUserId,
onThreadsChange: ({ content, threads }) =>
saveEditorState({ markdown: content, threads }),
resolveUsers,
threads,
}}
content={content}
contentFormat="markdown"
onContentChange={({ content }) => saveMarkdown(content)}
>
<Editor.View />
<Editor.CommentsSidebar />
</Editor.Root>Editor.CommentsSidebar renders Cubitt cards for threads, comments, replies,
reactions, and resolve state. Editor.View keeps BlockNote's floating comment
composer enabled for creating new comments. If the sidebar is closed on a
narrow layout, pass floatingThread to show the same Cubitt thread card as an
in-editor popover when a highlighted range is selected.
<Editor.View floatingThread={isCommentsSidebarClosed} />Thread Snapshots
comments.threads is the persisted thread snapshot. It stores the rich comment
thread data: comments, replies, authors, resolved state, reactions, and
metadata. Cubitt creates the BlockNote ThreadStore internally, normalizes
incoming snapshots, and emits comment updates through comments.onThreadsChange
with the current canonical content snapshot.
Creating or deleting a thread also updates the hidden range markers in the
document content. Persist the content and threads from onThreadsChange in
one write for comment mutations.
const comments = {
currentUserId,
onThreadsChange: ({ content, contentFormat, threads }) =>
saveEditorState({ content, contentFormat, threads }),
resolveUsers,
threads,
};
<Editor.Root comments={comments} contentFormat="blocknote">
<Editor.View />
<Editor.CommentsSidebar />
</Editor.Root>Resolve Users
comments.resolveUsers receives user IDs from the thread data and returns the
display users BlockNote needs for avatars and names.
const resolveUsers = async (userIds: string[]) => {
return userIds.map((id) => usersById[id]);
};Persistence Model
Store document content and thread data separately.
| Data | Owner | Notes |
|---|---|---|
| Document content | Consumer | Stored as BlockNote JSON or Markdown based on contentFormat. |
| Thread data | Consumer | Stored as the comments.threads snapshot. |
| Comment position | Cubitt + content | Persisted as hidden BlockNote inline marker nodes or Markdown marker tokens. |
For BlockNote JSON content, comment positions are stored as hidden inline marker nodes in the block content. For Markdown content, comment positions are stored as range marker tokens:
Text before §{comment-start:thread_123}§commented text§{comment-end:thread_123}§ text after.Editor hides those tokens while editing and converts them to BlockNote range
markers internally. Consumers should persist the emitted Markdown as-is so the
range can be restored on the next load.
When Markdown needs to render outside Editor in a surface that does not
understand Cubitt inline tokens, use stripEditorInlineTokens first:
import { stripEditorInlineTokens } from "@tilt-legal/cubitt-components/editor";
const displayMarkdown = stripEditorInlineTokens(markdown);
const displayWithoutComments = stripEditorInlineTokens(markdown, {
key: "comment",
});By default the helper removes every §{...}§ marker. Pass key to strip only
matching token namespaces, such as comment or ref.
Simple Mode
Use comments.features to turn off BlockNote comment features that are not
needed for a surface.
<Editor.Root
comments={{
features: {
reactions: false,
replies: false,
resolve: false,
},
currentUserId,
onThreadsChange: ({ content, threads }) =>
saveEditorState({ markdown: content, threads }),
resolveUsers,
threads,
}}
content={markdown}
contentFormat="markdown"
>
<Editor.View />
<Editor.CommentsSidebar />
</Editor.Root>Simple mode keeps edit and delete behavior available for the current user, while hiding or disabling replies, reactions, and resolve controls.
Disabling Comments
Omit comments to disable comments entirely. Editor.CommentsSidebar returns
null when comments are disabled, so the surrounding page layout can remove
the sidebar column.
<Editor.Root content={markdown} contentFormat="markdown">
<Editor.View />
<Editor.CommentsSidebar />
</Editor.Root>API Reference
stripEditorInlineTokens
function stripEditorInlineTokens(
value: string,
options?: { key?: string | readonly string[] }
): string;Removes Cubitt inline token markers from Markdown. Without key, every
§{...}§ token is removed. With key, only exact, colon-prefixed, or
hyphen-prefixed token namespaces are removed.
EditorCommentsConfig
| Field | Type | Description |
|---|---|---|
currentUserId | string | Current comment author id. |
threads | EditorCommentThreadData[] | Persisted thread snapshot. Date fields may be Date, ISO strings, or timestamps. |
onThreadsChange | (event: EditorCommentsChangeEvent) => void | Emits { contentFormat, content, threads } after comment edits. Persist this payload together so comment markers and thread data stay in sync. |
resolveUsers | (userIds: string[]) => Promise<EditorUser[]> | Resolves user IDs to BlockNote comment users. |
role | "editor" | "comment" | Optional BlockNote permission role for the current user. |
features | EditorCommentFeatures | Optional feature switches for simple comment surfaces. |
Editor.CommentsSidebar
| Prop | Type | Default | Description |
|---|---|---|---|
filter | "open" | "resolved" | "all" | "all" | Filters visible threads. |
sort | "position" | "recent-activity" | "oldest" | "position" | Orders threads by document position, recent activity, or oldest creation date. |
maxCommentsBeforeCollapse | number | 5 | Collapses long reply chains until the card is selected or expanded. |
Editor.View Comment Prop
| Prop | Type | Default | Description |
|---|---|---|---|
floatingThread | boolean | false | Shows a Cubitt floating thread card for the selected comment range. Use this when the sidebar is closed. |
EditorCommentFeatures
| Field | Type | Default | Description |
|---|---|---|---|
replies | boolean | true | Enables reply composition. |
reactions | boolean | true | Enables emoji reactions. |
resolve | boolean | true | Enables resolve and unresolve actions. |