Docs
Comments

Comments

BlockNote supports Comments, Comment Threads (replies) and emoji reactions out of the box.

To enable comments in your editor, you need to:

  • provide a resolveUsers so BlockNote can retrieve and display user information (names and avatars).
  • provide a ThreadStore so BlockNote can store and retrieve comment threads.
  • enable real-time collaboration (see Real-time collaboration)
const editor = useCreateBlockNote({
  resolveUsers: async (userIds: string[]) => {
    // return user information for the given userIds (see below)
  },
  comments: {
    threadStore: yourThreadStore, // see below
  },
  // ...
  collaboration: {
    // ... // see real-time collaboration docs
  },
});

Demo

"use client";
 
import {
  DefaultThreadStoreAuth,
  YjsThreadStore,
} from "@blocknote/core/comments";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
import { useCreateBlockNote } from "@blocknote/react";
import { YDocProvider, useYDoc, useYjsProvider } from "@y-sweet/react";
import { useMemo, useState } from "react";
 
import { SettingsSelect } from "./SettingsSelect.js";
import { HARDCODED_USERS, MyUserType, getRandomColor } from "./userdata.js";
 
import "./style.css";
 
// The resolveUsers function fetches information about your users
// (e.g. their name, avatar, etc.). Usually, you'd fetch this from your
// own database or user management system.
// Here, we just return the hardcoded users (from userdata.ts)
async function resolveUsers(userIds: string[]) {
  // fake a (slow) network request
  await new Promise((resolve) => setTimeout(resolve, 1000));
 
  return HARDCODED_USERS.filter((user) => userIds.includes(user.id));
}
 
// This follows the Y-Sweet example to setup a collabotive editor
// (but of course, you also use other collaboration providers
// see the docs for more information)
export default function App() {
  const docId = "my-blocknote-document-with-comments-1";
 
  return (
    <YDocProvider
      docId={docId}
      authEndpoint="https://demos.y-sweet.dev/api/auth">
      <Document />
    </YDocProvider>
  );
}
 
function Document() {
  const [activeUser, setActiveUser] = useState<MyUserType>(HARDCODED_USERS[0]);
 
  const provider = useYjsProvider();
 
  // take the Y.Doc collaborative document from Y-Sweet
  const doc = useYDoc();
 
  // setup the thread store which stores / and syncs thread / comment data
  const threadStore = useMemo(() => {
    // (alternative, use TiptapCollabProvider)
    // const provider = new TiptapCollabProvider({
    //   name: "test",
    //   baseUrl: "https://collab.yourdomain.com",
    //   appId: "test",
    //   document: doc,
    // });
    // return new TiptapThreadStore(
    //   activeUser.id,
    //   provider,
    //   new DefaultThreadStoreAuth(activeUser.id, activeUser.role)
    // );
    return new YjsThreadStore(
      activeUser.id,
      doc.getMap("threads"),
      new DefaultThreadStoreAuth(activeUser.id, activeUser.role)
    );
  }, [doc, activeUser]);
 
  // setup the editor with comments and collaboration
  const editor = useCreateBlockNote(
    {
      resolveUsers,
      comments: {
        threadStore,
      },
      collaboration: {
        provider,
        fragment: doc.getXmlFragment("blocknote"),
        user: { color: getRandomColor(), name: activeUser.username },
      },
    },
    [activeUser, threadStore]
  );
 
  return (
    <BlockNoteView
      className={"comments-main-container"}
      editor={editor}
      editable={activeUser.role === "editor"}>
      {/* We place user settings select within `BlockNoteView` as it uses
      BlockNote UI components and needs the context for them. */}
      <div className={"settings"}>
        <SettingsSelect
          label={"User"}
          items={HARDCODED_USERS.map((user) => ({
            text: `${user.username} (${
              user.role === "editor" ? "Editor" : "Commenter"
            })`,
            icon: null,
            onClick: () => setActiveUser(user),
            isSelected: user.id === activeUser.id,
          }))}
        />
      </div>
    </BlockNoteView>
  );
}
 

ThreadStores

A ThreadStore is used to store and retrieve comment threads. BlockNote is backend agnostic, so you can use any database or backend to store the threads. BlockNote comes with several built-in ThreadStore implementations:

YjsThreadStore

The YjsThreadStore provides direct Yjs-based storage for comments, storing thread data directly in the Yjs document. This implementation is ideal for simple collaborative setups where all users have write access to the document.

import { YjsThreadStore } from "@blocknote/core/comments";
 
const threadStore = new YjsThreadStore(
  userId, // The active user's ID
  yDoc.getMap("threads"), // Y.Map to store threads
  new DefaultThreadStoreAuth(userId, "editor"), // Authorization information, see below
);

Note: While this is the easiest to implement, it requires users to have write access to the Yjs document to leave comments. Also, without proper server-side validation, any user could technically modify other users' comments.

RESTYjsThreadStore

The RESTYjsThreadStore combines Yjs storage with a REST API backend, providing secure comment management while maintaining real-time collaboration. This implementation is ideal when you have strong authentication requirements, but is a little more work to set up.

In this implementation, data is written to the Yjs document via a REST API which can handle access control. Data is still retrieved from the Yjs document directly (after it's been updated by the REST API), this way all comment information automatically syncs between clients using the existing collaboration provider.

import {
  RESTYjsThreadStore,
  DefaultThreadStoreAuth,
} from "@blocknote/core/comments";
 
const threadStore = new RESTYjsThreadStore(
  "https://api.example.com/comments", // Base URL for the REST API
  {
    Authorization: "Bearer your-token", // Optional headers to add to requests
  },
  yDoc.getMap("threads"), // Y.Map to retrieve commend data from
  new DefaultThreadStoreAuth(userId, "editor"), // Authorization rules (see below)
);

An example implementation of the REST API can be found in the example repository (opens in a new tab).

Note: Because writes are executed via a REST API, the RESTYjsThreadStore is not suitable for local-first applications that should be able to add and edit comments offline.

TiptapThreadStore

The TiptapThreadStore integrates with Tiptap's collaboration provider for comment management. This implementation is designed specifically for use with Tiptap's collaborative editing features.

import {
  TiptapThreadStore,
  DefaultThreadStoreAuth,
} from "@blocknote/core/comments";
import { TiptapCollabProvider } from "@hocuspocus/provider";
 
// Create a TiptapCollabProvider (you probably have this already)
const provider = new TiptapCollabProvider({
  name: "test",
  baseUrl: "https://collab.yourdomain.com",
  appId: "test",
  document: doc,
});
 
// Create a TiptapThreadStore
const threadStore = new TiptapThreadStore(
  userId, // The active user's ID
  provider, // Tiptap collaboration provider
  new DefaultThreadStoreAuth(userId, "editor"), // Authorization rules (see below)
);

ThreadStoreAuth

The ThreadStoreAuth class defines the authorization rules for interacting with comments. Every ThreadStore implementation requires a ThreadStoreAuth instance. BlockNote uses the ThreadStoreAuth instance to deterine which interactions are allowed for the current user (for example, whether they can create a new comment, edit or delete a comment, etc.).

The DefaultThreadStoreAuth class provides a basic implementation of the ThreadStoreAuth class. It takes a user ID and a role ("comment" or "editor") and implements the rules. See the source code (opens in a new tab) for more details.

Note: The ThreadStoreAuth only used to show / hide options in the UI. To secure comment related data, you still need to implement your own server-side validation (e.g. using RESTYjsThreadStore and a secure REST API).

resolveUsers function

When a user interacts with a comment, the data is stored in the ThreadStore, along with the active user ID (as specified when initiating the ThreadStore).

To display comments, BlockNote needs to retrieve user information (such as the username and avatar) based on the user ID. To do this, you need to provide a resolveUsers function in the editor options.

This function is called with an array of user IDs, and should return an array of User objects in the same order.

type User = {
  id: string;
  username: string;
  avatarUrl: string;
};
 
async function myResolveUsers(userIds: string[]): Promise<User[]> {
  // fetch user information from your database / backend
  // and return an array of User objects
 
  return await callYourBackend(userIds);
 
  // Return a list of users
  return users;
}

Sidebar View

BlockNote also offers a different way of viewing and interacting with comments, via a sidebar instead of floating in the editor, using the ThreadsSidebar component:

"use client";
 
import {
  DefaultThreadStoreAuth,
  YjsThreadStore,
} from "@blocknote/core/comments";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
import {
  BlockNoteViewEditor,
  FloatingComposerController,
  ThreadsSidebar,
  useCreateBlockNote,
} from "@blocknote/react";
import { YDocProvider, useYDoc, useYjsProvider } from "@y-sweet/react";
import { useMemo, useState } from "react";
 
import { SettingsSelect } from "./SettingsSelect.js";
import { HARDCODED_USERS, MyUserType, getRandomColor } from "./userdata.js";
 
import "./style.css";
 
// The resolveUsers function fetches information about your users
// (e.g. their name, avatar, etc.). Usually, you'd fetch this from your
// own database or user management system.
// Here, we just return the hardcoded users (from userdata.ts)
async function resolveUsers(userIds: string[]) {
  // fake a (slow) network request
  await new Promise((resolve) => setTimeout(resolve, 1000));
 
  return HARDCODED_USERS.filter((user) => userIds.includes(user.id));
}
 
// This follows the Y-Sweet example to setup a collabotive editor
// (but of course, you also use other collaboration providers
// see the docs for more information)
export default function App() {
  const docId = "my-blocknote-document-with-comments-2";
 
  return (
    <YDocProvider
      docId={docId}
      authEndpoint="https://demos.y-sweet.dev/api/auth">
      <Document />
    </YDocProvider>
  );
}
 
function Document() {
  const [activeUser, setActiveUser] = useState<MyUserType>(HARDCODED_USERS[0]);
  const [commentFilter, setCommentFilter] = useState<
    "open" | "resolved" | "all"
  >("open");
  const [commentSort, setCommentSort] = useState<
    "position" | "recent-activity" | "oldest"
  >("position");
 
  const provider = useYjsProvider();
 
  // take the Y.Doc collaborative document from Y-Sweet
  const doc = useYDoc();
 
  // setup the thread store which stores / and syncs thread / comment data
  const threadStore = useMemo(() => {
    // (alternative, use TiptapCollabProvider)
    // const provider = new TiptapCollabProvider({
    //   name: "test",
    //   baseUrl: "https://collab.yourdomain.com",
    //   appId: "test",
    //   document: doc,
    // });
    // return new TiptapThreadStore(
    //   activeUser.id,
    //   provider,
    //   new DefaultThreadStoreAuth(activeUser.id, activeUser.role)
    // );
    return new YjsThreadStore(
      activeUser.id,
      doc.getMap("threads"),
      new DefaultThreadStoreAuth(activeUser.id, activeUser.role)
    );
  }, [doc, activeUser]);
 
  // setup the editor with comments and collaboration
  const editor = useCreateBlockNote(
    {
      resolveUsers,
      comments: {
        threadStore,
      },
      collaboration: {
        provider,
        fragment: doc.getXmlFragment("blocknote"),
        user: { color: getRandomColor(), name: activeUser.username },
      },
    },
    [activeUser, threadStore]
  );
 
  return (
    <BlockNoteView
      className={"sidebar-comments-main-container"}
      editor={editor}
      editable={activeUser.role === "editor"}
      // In other examples, `BlockNoteView` renders both editor element itself,
      // and the container element which contains the necessary context for
      // BlockNote UI components. However, in this example, we want more control
      // over the rendering of the editor, so we set `renderEditor` to `false`.
      // Now, `BlockNoteView` will only render the container element, and we can
      // render the editor element anywhere we want using `BlockNoteEditorView`.
      renderEditor={false}
      // We also disable the default rendering of comments in the editor, as we
      // want to render them in the `ThreadsSidebar` component instead.
      comments={false}>
      {/* We place the editor, the sidebar, and any settings selects within
      `BlockNoteView` as they use BlockNote UI components and need the context
      for them. */}
      <div className={"editor-layout-wrapper"}>
        <div className={"editor-section"}>
          <h1>Editor</h1>
          <div className={"settings"}>
            <SettingsSelect
              label={"User"}
              items={HARDCODED_USERS.map((user) => ({
                text: `${user.username} (${
                  user.role === "editor" ? "Editor" : "Commenter"
                })`,
                icon: null,
                onClick: () => {
                  setActiveUser(user);
                },
                isSelected: user.id === activeUser.id,
              }))}
            />
          </div>
          {/* Because we set `renderEditor` to false, we can now manually place
          `BlockNoteViewEditor` (the actual editor component) in its own
          section below the user settings select. */}
          <BlockNoteViewEditor />
          {/* Since we disabled rendering of comments with `comments={false}`,
          we need to re-add the floating composer, which is the UI element that
          appears when creating new threads. */}
          <FloatingComposerController />
        </div>
      </div>
      {/* We also place the `ThreadsSidebar` component in its own section,
      along with settings for filtering and sorting. */}
      <div className={"threads-sidebar-section"}>
        <h1>Comments</h1>
        <div className={"settings"}>
          <SettingsSelect
            label={"Filter"}
            items={[
              {
                text: "All",
                icon: null,
                onClick: () => setCommentFilter("all"),
                isSelected: commentFilter === "all",
              },
              {
                text: "Open",
                icon: null,
                onClick: () => setCommentFilter("open"),
                isSelected: commentFilter === "open",
              },
              {
                text: "Resolved",
                icon: null,
                onClick: () => setCommentFilter("resolved"),
                isSelected: commentFilter === "resolved",
              },
            ]}
          />
          <SettingsSelect
            label={"Sort"}
            items={[
              {
                text: "Position",
                icon: null,
                onClick: () => setCommentSort("position"),
                isSelected: commentSort === "position",
              },
              {
                text: "Recent activity",
                icon: null,
                onClick: () => setCommentSort("recent-activity"),
                isSelected: commentSort === "recent-activity",
              },
              {
                text: "Oldest",
                icon: null,
                onClick: () => setCommentSort("oldest"),
                isSelected: commentSort === "oldest",
              },
            ]}
          />
        </div>
        <ThreadsSidebar filter={commentFilter} sort={commentSort} />
      </div>
    </BlockNoteView>
  );
}
 

The only requirement for ThreadsSidebar is that it should be placed somewhere within your BlockNoteView, other than that you can position and style it however you want.

ThreadsSidebar also takes 2 props:

filter: Filter the comments in the sidebar. Can pass "open", "resolved", or "all", to only show open, resolved, or all comments. Defaults to "all".

sort: Sort the comments in the sidebar. Can pass "position", "recent-activity", or "oldest". Sorting by "recent-activity" uses the most recently added comment to sort threads, while "oldest" uses the thread creation date. Sorting by "position" puts comments in the same order as their reference text in the editor. Defaults to "position".

maxCommentsBeforeCollapse: The maximum number of comments that can be in a thread before the replies get collapsed. Defaults to 5.

See here (opens in a new tab) for a standalone example of the ThreadsSidebar component.