Skip to content

Editor Documentation

Complete guide to the Plate.js rich text editor in Constellation.

Overview

Constellation uses Plate.js (built on Slate), a powerful headless editor for rich content creation.

Version: 52.x
Documentation: plate.dev


Editor Features

Plugins Included

Plugin Feature
Basic Nodes Paragraphs, headings, quotes
Basic Styles Bold, italic, underline, code
Lists Ordered, unordered, to-do lists
Code Block Syntax highlighting
Table Table creation and editing
Link Hyperlink insertion
Media Images, videos, embeds
Mention @mention users
Emoji Emoji picker
Slash Command / command palette
Comment Collaborative comments
Break Block insertion (Cmd+Enter)

Basic Usage

PlateEditor Component

import { PlateEditor } from '@/src/components/editor/PlateEditor';

export function EditorPage() {
  const [content, setContent] = useState([]);

  return (
    <PlateEditor
      content={content}
      onChange={setContent}
      readOnly={false}
    />
  );
}

Props

interface PlateEditorProps {
  content?: SlateElement[];
  onChange?: (content: SlateElement[]) => void;
  readOnly?: boolean;
  placeholder?: string;
  disabled?: boolean;
  plugins?: PluginOptions[];
  className?: string;
}

Editor Content Structure

Document Format

// Content is an array of blocks
const content = [
  {
    type: 'p',
    children: [
      {
        text: 'This is a paragraph with ',
      },
      {
        text: 'bold text',
        bold: true,
      },
      {
        text: '.',
      },
    ],
  },
  {
    type: 'h1',
    children: [{ text: 'Heading' }],
  },
];

Common Element Types

// Paragraph
{ type: 'p', children: [{ text: 'Content' }] }

// Headings
{ type: 'h1', children: [{ text: 'Heading' }] }
{ type: 'h2', children: [{ text: 'Subheading' }] }

// Lists
{
  type: 'ul',
  children: [
    { type: 'li', children: [{ text: 'Item 1' }] },
    { type: 'li', children: [{ text: 'Item 2' }] },
  ],
}

// Code block
{
  type: 'code_block',
  language: 'javascript',
  children: [{ text: 'const x = 1;' }],
}

// Quote
{ type: 'blockquote', children: [{ text: 'Quote text' }] }

// Table
{
  type: 'table',
  children: [
    {
      type: 'tr',
      children: [
        { type: 'td', children: [{ text: 'Cell' }] },
      ],
    },
  ],
}

Toolbar

EditorToolbar Component

import { EditorToolbar } from '@/src/components/editor/EditorToolbar';

export function EditorWithToolbar() {
  return (
    <>
      <EditorToolbar />
      <PlateEditor />
    </>
  );
}

Toolbar Buttons

Button Shortcut Action
Bold Cmd+B Toggle bold
Italic Cmd+I Toggle italic
Underline Cmd+U Toggle underline
Code Cmd+` Toggle inline code
Bullet List Cmd+Shift+8 Create bullet list
Numbered List Cmd+Shift+7 Create numbered list
Quote Cmd+Shift+. Create blockquote
Code Block Cmd+Alt+C Create code block
Link Cmd+K Insert link
Image - Insert image
Table - Insert table

Mentions Plugin

Usage

Type @ to trigger mentions:

// Editor automatically detects and shows users
// Select user to insert mention

// Output:
{
  type: 'mention',
  value: 'user-123',
  name: 'John Doe',
  children: [{ text: '@John Doe' }],
}

Configuring Mention Sources

const mentionOptions = {
  trigger: '@',
  search: async (query: string) => {
    // Search users matching query
    const users = await apiClient.get('/users/search', {
      params: { q: query },
    });
    return users.map(user => ({
      key: user.id,
      label: user.name,
    }));
  },
};

Emoji Plugin

Usage

Type : to trigger emoji picker:

:smile: โ†’ ๐Ÿ˜Š
:fire: โ†’ ๐Ÿ”ฅ
:rocket: โ†’ ๐Ÿš€

Custom Emoji List

const emojiOptions = {
  trigger: ':',
  data: emojiData, // @emoji-mart/data
};

Slash Commands

Usage

Type / to see available commands:

/heading โ†’ Insert heading
/list โ†’ Insert bullet list
/table โ†’ Insert table
/code โ†’ Insert code block
/quote โ†’ Insert quote
/image โ†’ Insert image

Creating Custom Commands

const slashCommandPlugins = [
  {
    name: 'heading',
    description: 'Insert heading',
    onSelect: (editor) => {
      insertElement(editor, { type: 'h1', children: [{ text: '' }] });
    },
  },
];

Comments

Adding Comments

// Select text and press Cmd+Shift+M
// Or click comment button in toolbar
// Enter comment text
// Comments appear in sidebar

Comment Structure

{
  type: 'comment',
  data: {
    id: 'comment-123',
    author: 'user-456',
    text: 'This needs fixing',
    resolved: false,
    replies: [],
  },
  children: [{ text: 'commented text' }],
}

Serialization

Save to JSON

// Get editor content
const content = editor.getChildren();
const json = JSON.stringify(content);

// Save to database
await apiClient.post('/documents', { content: json });

Load from JSON

const json = await apiClient.get(`/documents/${id}`);
const content = JSON.parse(json.content);
setEditorContent(content);

HTML Export

import { serializeHtml } from '@plate.js/markdown';

const html = serializeHtml(editor, {
  nodes: { /* config */ },
});

Markdown Import/Export

import { serializeMarkdown } from '@plate.js/markdown';

// Export to markdown
const markdown = serializeMarkdown(editor);

// Import from markdown
const content = deserializeMarkdown(markdownText);

Collaborative Editing

Yjs Integration

import { useYjs } from '@/src/hooks/use-yjs';

export function CollaborativeEditor() {
  const { yText, connected } = useYjs('document-123');

  return (
    <PlateEditor
      content={yText.toString()}
      onChange={(content) => {
        yText.delete(0, yText.length);
        yText.insert(0, JSON.stringify(content));
      }}
    />
  );
}

Real-time Sync

// Connect via Hocuspocus
const provider = hocuspocusProvider.connect('document-id', {
  awareness: true,
  onUpdate: (data) => {
    // Update when other users edit
  },
});

Customization

Custom Plugins

const customPlugin = {
  name: 'customElement',
  handler: ({ isActive, type }) => ({
    isActive: isActive(type),
    type,
  }),
};

Custom Renders

// Custom render for specific element type
const renderers = {
  [ELEMENT_H1]: (props) => (
    <h1 className="text-3xl font-bold" {...props}>
      {props.children}
    </h1>
  ),
};

Theme Customization

const editorTheme = {
  colors: {
    primary: '#0066cc',
    background: '#ffffff',
  },
  typography: {
    fontFamily: 'Inter, sans-serif',
  },
};

API Reference

Editor Methods

const editor = useEditor();

// Insert element
editor.insertNode({ type: 'p', children: [{ text: 'New paragraph' }] });

// Delete
editor.delete();

// Get content
editor.getChildren();

// Set content
editor.setNodes(newContent);

// Undo/Redo
editor.undo();
editor.redo();

// Check if dirty (unsaved changes)
editor.isDirty();

Plate UI Components

Located: src/components/plate-ui/

Component Purpose
Plate Main editor wrapper
PlateContent Editable area
PlateToolbar Toolbar container
ToolbarButton Toolbar button
ToolbarGroup Grouped buttons

Performance Tips

โœ… Best Practices

  • โœ… Debounce onChange to prevent excessive updates
  • โœ… Memoize editor instance
  • โœ… Lazy load plugins
  • โœ… Optimize re-renders
  • โœ… Use controlled component properly

Example: Debounced Save

import { useDebounce } from '@/src/hooks/use-debounce';

export function EditorWithAutosave() {
  const [content, setContent] = useState([]);
  const debouncedContent = useDebounce(content, 1000);

  useEffect(() => {
    if (debouncedContent) {
      // Auto-save
      apiClient.post('/documents/autosave', {
        content: debouncedContent,
      });
    }
  }, [debouncedContent]);

  return <PlateEditor content={content} onChange={setContent} />;
}

Troubleshooting

Editor Not Showing

  1. Verify PlateEditor component is imported
  2. Check Plate UI plugins are loaded
  3. Verify CSS is included
  4. Check for console errors

Formatting Not Working

  1. Ensure toolbar buttons are connected
  2. Verify plugins are enabled
  3. Check TypeScript types

Real-time Sync Issues

  1. Verify Hocuspocus connection
  2. Check WebSocket URL in env
  3. Verify document ID is consistent

Examples

Basic Editor

'use client';

import { useState } from 'react';
import { PlateEditor } from '@/src/components/editor/PlateEditor';
import { EditorToolbar } from '@/src/components/editor/EditorToolbar';

export function SimpleEditor() {
  const [content, setContent] = useState([
    {
      type: 'p',
      children: [{ text: 'Start typing...' }],
    },
  ]);

  return (
    <div>
      <EditorToolbar />
      <PlateEditor content={content} onChange={setContent} />
    </div>
  );
}

Read-only Viewer

<PlateEditor content={content} readOnly={true} />

Collaborative Document

import { useYjs } from '@/src/hooks/use-yjs';

export function CollaborativeDoc() {
  const { yText, connected } = useYjs('doc-id');

  return (
    <PlateEditor
      content={yText.toString()}
      onChange={(content) => {
        // Update Yjs text
      }}
    />
  );
}


Last Updated: January 2026
Official Docs: plate.dev