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.

DataOwnerNotes
Document contentConsumerStored as BlockNote JSON or Markdown based on contentFormat.
Thread dataConsumerStored as the comments.threads snapshot.
Comment positionCubitt + contentPersisted 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

FieldTypeDescription
currentUserIdstringCurrent comment author id.
threadsEditorCommentThreadData[]Persisted thread snapshot. Date fields may be Date, ISO strings, or timestamps.
onThreadsChange(event: EditorCommentsChangeEvent) => voidEmits { 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.
featuresEditorCommentFeaturesOptional feature switches for simple comment surfaces.

Editor.CommentsSidebar

PropTypeDefaultDescription
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.
maxCommentsBeforeCollapsenumber5Collapses long reply chains until the card is selected or expanded.

Editor.View Comment Prop

PropTypeDefaultDescription
floatingThreadbooleanfalseShows a Cubitt floating thread card for the selected comment range. Use this when the sidebar is closed.

EditorCommentFeatures

FieldTypeDefaultDescription
repliesbooleantrueEnables reply composition.
reactionsbooleantrueEnables emoji reactions.
resolvebooleantrueEnables resolve and unresolve actions.

On this page