From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Stream
    • Copilot
  • Discussion
    • Comments
    • Suggestion
    • Basic Blocks
      • Blockquote
      • Heading
      • Horizontal Rule
    • Callout
    • Code Block
    • Column
    • Date
    • Equation
    • Link
    • List Classic
    • Media
    • MentionElement
    • Table
    • Table of Contents
    • Footnote
    • Toggle
  • Marks
    • Bold
    • Italic
    • Underline
    • Code
    • Highlight
    • Keyboard Input
    • Strikethrough
    • Subscript
    • Superscript
      • Font
      • Line Height
      • Text Align
    • Indent
    • List
      • Exit Break
      • Single Block
      • Trailing Block
    • Autoformat
    • Block Menu
    • Block Placeholder
    • Combobox
      • Emoji
      • MentionElement
      • Slash Command
    • Cursor Overlay
    • Drag & Drop
    • Navigation Feedback
    • Tabbable
    • Toolbar
    • Yjs
    • Multi SelectEditor
    • CSV
    • DOCX
    • HTML
    • Markdown

Discussion

PreviousNext
PlusBlock Discussion
Loading…
CopilotComments

On This Page

FeaturesKit UsageInstallationAdd KitManual UsageInstallationCreate PluginAdd PluginPlate PlusPluginsdiscussionPluginSelectorscurrentUseruserTypesTDiscussionUserData
Build your editor
Production-ready AI template and reusable components.
Get all-access

Features

  • User Management: Store and manage user data with avatars and names
  • Discussion Threads: Manage discussion data structures with comments
  • Current User Tracking: Track the current active user for collaboration
  • Data Storage: Pure UI plugin for storing collaboration state
  • Selector API: Easy access to user data through plugin selectors
Report an issue

Kit Usage

Installation

The fastest way to add discussion functionality is with the DiscussionKit, which includes the pre-configured discussionPlugin along with its Plate UI components.

'use client';
 
import { definePlatePlugin } from 'platejs/react';
 
import { BlockDiscussion } from '@/components/editor/block-discussion';
import type { TComment } from '@/components/editor/comment';
 
export type TDiscussion = {
  id: string;
  comments: TComment[];
  createdAt: Date;
  isResolved: boolean;
  userId: string;
  documentContent?: string;
};
 
export type DiscussionPluginState = {
  currentUserId: string;
  discussions: TDiscussion[];
  users: Record<
    string,
    { id: string; avatarUrl: string; name: string; hue?: number }
  >;
};
 
const BLOCK_SUGGESTION_SELECTOR = '[data-block-suggestion="true"]';
const PLAYGROUND_DISCUSSION_CREATED_AT = 1_704_067_200_000;
const playgroundDiscussionDate = (offset = 0) =>
  new Date(PLAYGROUND_DISCUSSION_CREATED_AT + offset);
 
const getTargetElement = (target: EventTarget | null) => {
  if (target instanceof HTMLElement) return target;
  if (target instanceof Node) return target.parentElement;
 
  return null;
};
 
export const getDiscussionClickTarget = ({
  selector,
  target,
}: {
  selector: string;
  target: EventTarget | null;
}) => {
  const element = getTargetElement(target);
 
  if (!element) return null;
 
  return element.closest(selector);
};
 
export const getDiscussionBlockClickTarget = ({
  selector = BLOCK_SUGGESTION_SELECTOR,
  target,
}: {
  selector?: string;
  target: EventTarget | null;
}) =>
  getDiscussionClickTarget({
    selector,
    target,
  });
 
const discussionsData: TDiscussion[] = [
  {
    id: 'discussion1',
    comments: [
      {
        id: 'comment1',
        contentRich: [
          {
            children: [
              {
                text: 'Comments are a great way to provide feedback and discuss changes.',
              },
            ],
            type: 'paragraph',
          },
        ],
        createdAt: playgroundDiscussionDate(-600_000),
        discussionId: 'discussion1',
        isEdited: false,
        userId: 'charlie',
      },
      {
        id: 'comment2',
        contentRich: [
          {
            children: [
              {
                text: 'Agreed! The link to the docs makes it easy to learn more.',
              },
            ],
            type: 'paragraph',
          },
        ],
        createdAt: playgroundDiscussionDate(-500_000),
        discussionId: 'discussion1',
        isEdited: false,
        userId: 'bob',
      },
    ],
    createdAt: playgroundDiscussionDate(),
    documentContent: 'comments',
    isResolved: false,
    userId: 'charlie',
  },
  {
    id: 'discussion2',
    comments: [
      {
        id: 'comment1',
        contentRich: [
          {
            children: [
              {
                text: 'Nice demonstration of overlapping annotations with both comments and suggestions!',
              },
            ],
            type: 'paragraph',
          },
        ],
        createdAt: playgroundDiscussionDate(-300_000),
        discussionId: 'discussion2',
        isEdited: false,
        userId: 'bob',
      },
      {
        id: 'comment2',
        contentRich: [
          {
            children: [
              {
                text: 'This helps users understand how powerful the editor can be.',
              },
            ],
            type: 'paragraph',
          },
        ],
        createdAt: playgroundDiscussionDate(-200_000),
        discussionId: 'discussion2',
        isEdited: false,
        userId: 'charlie',
      },
    ],
    createdAt: playgroundDiscussionDate(),
    documentContent: 'overlapping',
    isResolved: false,
    userId: 'bob',
  },
];
 
const avatarUrl = (seed: string) =>
  `https://api.dicebear.com/9.x/glass/svg?seed=${seed}`;
 
const usersData: Record<
  string,
  { id: string; avatarUrl: string; name: string; hue?: number }
> = {
  alice: {
    id: 'alice',
    avatarUrl: avatarUrl('alice6'),
    name: 'Alice',
  },
  bob: {
    id: 'bob',
    avatarUrl: avatarUrl('bob4'),
    name: 'Bob',
  },
  charlie: {
    id: 'charlie',
    avatarUrl: avatarUrl('charlie2'),
    name: 'Charlie',
  },
};
 
const initialState: DiscussionPluginState = {
  currentUserId: 'alice',
  discussions: discussionsData,
  users: usersData,
};
 
// This plugin is purely UI. It's only used to store the discussions and users data
export const discussionPlugin = definePlatePlugin('discussion', {
  initialState,
  selectors: {
    currentUser: (state) => state.users[state.currentUserId],
    user: (state, id: string) => state.users[id],
  },
}).configure({
  render: { aboveNodes: BlockDiscussion },
});
 
export const DiscussionKit = [discussionPlugin];
'use client';
 
import { definePlatePlugin } from 'platejs/react';
 
import { BlockDiscussion } from '@/components/editor/block-discussion';
import type { TComment } from '@/components/editor/comment';
 
export type TDiscussion = {
  id: string;
  comments: TComment[];
  createdAt: Date;
  isResolved: boolean;
  userId: string;
  documentContent?: string;
};
 
export type DiscussionPluginState = {
















































































































































































  • BlockDiscussion: Renders discussion UI above nodes

Add Kit

import { createPlateEditor } from 'platejs/react';
import { DiscussionKit } from '@/components/editor/discussion';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    ...DiscussionKit,
  ],
});
import { createPlateEditor } from 'platejs/react';
import { DiscussionKit } from '@/components/editor/discussion';
 
const editor = createPlateEditor




Manual Usage

Installation

pnpm add @platejs/comment @platejs/suggestion
pnpm add @platejs/comment @platejs/suggestion

Create Plugin

import { definePlatePlugin } from 'platejs/react';
import { BlockDiscussion } from '@/components/editor/block-discussion';
 
export interface TDiscussion {
  id: string;
  comments: TComment[];
  createdAt: Date;
  isResolved: boolean;
  userId: string;
  documentContent?: string;
}
 
const usersData = {
  alice: {
    id: 'alice',
    avatarUrl: 'https://api.dicebear.com/9.x/glass/svg?seed=alice6',
    name: 'Alice',
  },




























import { definePlatePlugin } from 'platejs/react';
import { BlockDiscussion } from '@/components/editor/block-discussion';
 
export interface TDiscussion {
  id: string;
  comments: TComment[];
  createdAt: Date;
  isResolved: boolean;
  userId: string;
  documentContent?: string;
}
 
const usersData = {
  alice: {
    id: 'alice',
    avatarUrl: 'https://api.dicebear.com/9.x/glass/svg?seed=alice6',
    name: 'Alice'





























  • initialState.currentUserId: ID of the current active user
  • initialState.discussions: Array of discussion data structures
  • initialState.users: Object mapping user IDs to user data
  • render.aboveNodes: Renders BlockDiscussion above nodes for discussion UI
  • selectors.currentUser: Gets the current user data
  • selectors.user: Gets user data by ID

Add Plugin

import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    discussionPlugin,
  ],
});
import { createPlateEditor } from 'platejs/react';
 
const editor = createPlateEditor({
  plugins: [
    // ...otherPlugins,
    discussionPlugin,
  ],
});

Plate Plus

  • Full stack example for Suggestion and Comment
  • Floating comments & suggestions UI with better user experience
  • Comment rendered with Plate editor
  • Discussion list in the sidebar
Get the code

Plugins

discussionPlugin

Pure UI plugin for managing collaboration state including users and discussion data.

Options

    ID of the current active user in the collaboration session.

    Array of discussion objects containing comments and metadata.

    Object mapping user IDs to user information including name and avatar.

Selectors

currentUser

Gets the current user data.

ReturnsUserData

    The current user's data including id, name, and avatarUrl.

user

Gets user data by ID.

Parameters

    The user ID to look up.

ReturnsUserData | undefined

    The user data if found, undefined otherwise.

Types

TDiscussion

Discussion data structure containing comments and metadata.

Attributes

    Unique identifier for the discussion.

    Array of comments in the discussion thread.

    When the discussion was created.

    Whether the discussion has been resolved.

    ID of the user who created the discussion.

    Content from the document related to this discussion.

UserData

User information structure for collaboration.

Attributes

    Unique identifier for the user.

    Display name of the user.

    URL for the user's avatar image.

    Optional color hue for user identification.

currentUserId: string;
discussions: TDiscussion[];
users: Record<
string,
{ id: string; avatarUrl: string; name: string; hue?: number }
>;
};
const BLOCK_SUGGESTION_SELECTOR = '[data-block-suggestion="true"]';
const PLAYGROUND_DISCUSSION_CREATED_AT = 1_704_067_200_000;
const playgroundDiscussionDate = (offset = 0) =>
new Date(PLAYGROUND_DISCUSSION_CREATED_AT + offset);
const getTargetElement = (target: EventTarget | null) => {
if (target instanceof HTMLElement) return target;
if (target instanceof Node) return target.parentElement;
return null;
};
export const getDiscussionClickTarget = ({
selector,
target,
}: {
selector: string;
target: EventTarget | null;
}) => {
const element = getTargetElement(target);
if (!element) return null;
return element.closest(selector);
};
export const getDiscussionBlockClickTarget = ({
selector = BLOCK_SUGGESTION_SELECTOR,
target,
}: {
selector?: string;
target: EventTarget | null;
}) =>
getDiscussionClickTarget({
selector,
target,
});
const discussionsData: TDiscussion[] = [
{
id: 'discussion1',
comments: [
{
id: 'comment1',
contentRich: [
{
children: [
{
text: 'Comments are a great way to provide feedback and discuss changes.',
},
],
type: 'paragraph',
},
],
createdAt: playgroundDiscussionDate(-600_000),
discussionId: 'discussion1',
isEdited: false,
userId: 'charlie',
},
{
id: 'comment2',
contentRich: [
{
children: [
{
text: 'Agreed! The link to the docs makes it easy to learn more.',
},
],
type: 'paragraph',
},
],
createdAt: playgroundDiscussionDate(-500_000),
discussionId: 'discussion1',
isEdited: false,
userId: 'bob',
},
],
createdAt: playgroundDiscussionDate(),
documentContent: 'comments',
isResolved: false,
userId: 'charlie',
},
{
id: 'discussion2',
comments: [
{
id: 'comment1',
contentRich: [
{
children: [
{
text: 'Nice demonstration of overlapping annotations with both comments and suggestions!',
},
],
type: 'paragraph',
},
],
createdAt: playgroundDiscussionDate(-300_000),
discussionId: 'discussion2',
isEdited: false,
userId: 'bob',
},
{
id: 'comment2',
contentRich: [
{
children: [
{
text: 'This helps users understand how powerful the editor can be.',
},
],
type: 'paragraph',
},
],
createdAt: playgroundDiscussionDate(-200_000),
discussionId: 'discussion2',
isEdited: false,
userId: 'charlie',
},
],
createdAt: playgroundDiscussionDate(),
documentContent: 'overlapping',
isResolved: false,
userId: 'bob',
},
];
const avatarUrl = (seed: string) =>
`https://api.dicebear.com/9.x/glass/svg?seed=${seed}`;
const usersData: Record<
string,
{ id: string; avatarUrl: string; name: string; hue?: number }
> = {
alice: {
id: 'alice',
avatarUrl: avatarUrl('alice6'),
name: 'Alice',
},
bob: {
id: 'bob',
avatarUrl: avatarUrl('bob4'),
name: 'Bob',
},
charlie: {
id: 'charlie',
avatarUrl: avatarUrl('charlie2'),
name: 'Charlie',
},
};
const initialState: DiscussionPluginState = {
currentUserId: 'alice',
discussions: discussionsData,
users: usersData,
};
// This plugin is purely UI. It's only used to store the discussions and users data
export const discussionPlugin = definePlatePlugin('discussion', {
initialState,
selectors: {
currentUser: (state) => state.users[state.currentUserId],
user: (state, id: string) => state.users[id],
},
}).configure({
render: { aboveNodes: BlockDiscussion },
});
export const DiscussionKit = [discussionPlugin];
({
plugins: [
// ...otherPlugins,
...DiscussionKit,
],
});
bob: {
id: 'bob',
avatarUrl: 'https://api.dicebear.com/9.x/glass/svg?seed=bob4',
name: 'Bob',
},
};
export type DiscussionPluginState = {
currentUserId: string;
discussions: TDiscussion[];
users: Record<string, { avatarUrl: string; id: string; name: string }>;
};
const initialState: DiscussionPluginState = {
currentUserId: 'alice',
discussions: [],
users: usersData,
};
export const discussionPlugin = definePlatePlugin('discussion', {
initialState,
selectors: {
currentUser: (state) => state.users[state.currentUserId],
user: (state, id: string) => state.users[id],
},
})
.configure({
render: { aboveNodes: BlockDiscussion },
});
,
},
bob: {
id: 'bob',
avatarUrl: 'https://api.dicebear.com/9.x/glass/svg?seed=bob4',
name: 'Bob',
},
};
export type DiscussionPluginState = {
currentUserId: string;
discussions: TDiscussion[];
users: Record<string, { avatarUrl: string; id: string; name: string }>;
};
const initialState: DiscussionPluginState = {
currentUserId: 'alice',
discussions: [],
users: usersData,
};
export const discussionPlugin = definePlatePlugin('discussion', {
initialState,
selectors: {
currentUser: (state) => state.users[state.currentUserId],
user: (state, id: string) => state.users[id],
},
})
.configure({
render: { aboveNodes: BlockDiscussion },
});