# Plate — Rich-Text Editor Framework Plate is a comprehensive toolkit for building rich-text editors in React, built on top of [Slate](https://docs.slatejs.org/) and [Slate React](https://github.com/ianstormtaylor/slate). It provides a robust, React-focused plugin system with SSR support, a framework-agnostic core, and a wide library of headless plugins covering marks, block elements, tables, media, AI, collaboration, serialization, and more. The architecture is built on three pillars: a **Core Engine** (plugin system, typed API/transforms, editor state), **Extensible Plugins** (composable, zero-config or fully customizable), and **Plate UI** (shadcn/ui-compatible components that you own via copy-paste). Plate supports Next.js, Vite, Remix, React Server Components, and plain Node.js environments. The plugin system is the heart of Plate. Every feature — from bold marks to real-time Yjs collaboration — is encapsulated as a plugin that exposes typed API methods (`editor.api.*`), transforms (`editor.tf.*`), keyboard shortcuts, event handlers, HTML parsers, and rendering components. Plugins are created with `createPlatePlugin` / `createTPlatePlugin` and composed into an editor via `createPlateEditor` (browser) or `createSlateEditor` (Node.js/SSR). Plate UI complements the headless system with copy-paste shadcn components installable via `npx shadcn@latest add @plate/`. --- ## Core APIs & Key Functions ### `createPlateEditor` / `usePlateEditor` — Create an editor instance `createPlateEditor` returns a ready-to-use `PlateEditor` with all plugins registered and typed API/transforms attached. `usePlateEditor` is the memoized React hook variant for use inside components. ```tsx import { createPlateEditor, usePlateEditor } from 'platejs/react'; import { BoldPlugin, ItalicPlugin, UnderlinePlugin, H1Plugin, H2Plugin, H3Plugin, BlockquotePlugin, } from '@platejs/basic-nodes/react'; import type { Value } from 'platejs'; const initialValue: Value = [ { type: 'h1', children: [{ text: 'Hello, Plate!' }] }, { type: 'p', children: [ { text: 'This is ' }, { text: 'bold', bold: true }, { text: ' and ' }, { text: 'italic', italic: true }, { text: '.' }, ], }, ]; // Non-React usage (e.g. server scripts, tests) const editorStatic = createPlateEditor({ plugins: [BoldPlugin, ItalicPlugin, H1Plugin], value: initialValue, maxLength: 5000, id: 'my-editor', shouldNormalizeEditor: true, autoSelect: 'end', }); // React component function MyEditor() { const editor = usePlateEditor({ plugins: [BoldPlugin, ItalicPlugin, UnderlinePlugin, H1Plugin, H2Plugin, H3Plugin, BlockquotePlugin], value: () => { const saved = localStorage.getItem('doc'); return saved ? JSON.parse(saved) : initialValue; }, nodeId: { idCreator: () => crypto.randomUUID() }, // custom ID generator }); return
{/* use editor below */}
; } ``` --- ### `` / `` — Mount the editor `` is the React context provider wrapping all editor hooks. `` (or the Plate UI ``) is the editable surface. `onChange` fires on every content change. ```tsx import { Plate, PlateContent, usePlateEditor } from 'platejs/react'; import type { Value } from 'platejs'; export default function App() { const editor = usePlateEditor({ plugins: [/* ...plugins */], value: [{ type: 'p', children: [{ text: '' }] }], }); const handleChange = ({ value }: { value: Value }) => { localStorage.setItem('draft', JSON.stringify(value)); }; return ( ); } ``` --- ### `createSlateEditor` — Framework-agnostic / Node.js editor For server-side processing, RSC, or Node.js scripts, use `createSlateEditor` from `platejs`. Never import `/react` subpaths in Node.js. ```typescript // scripts/process-content.ts import { createSlateEditor } from 'platejs'; import { BaseBoldPlugin, BaseItalicPlugin, BaseH1Plugin, BaseBlockquotePlugin, } from '@platejs/basic-nodes'; // NOT /react const editor = createSlateEditor({ plugins: [BaseBoldPlugin, BaseItalicPlugin, BaseH1Plugin, BaseBlockquotePlugin], value: [ { type: 'h1', children: [{ text: 'Server-side document' }] }, { type: 'p', children: [{ text: 'Process this content.' }] }, ], }); // Traverse children for (const node of editor.children) { console.info(node.type, node.children); } // Apply transforms without React editor.tf.insertNodes({ type: 'p', children: [{ text: 'Appended!' }] }); console.info(JSON.stringify(editor.children, null, 2)); ``` --- ### `createPlatePlugin` / `createTPlatePlugin` — Define plugins `createPlatePlugin` creates a new plugin with inferred types. `createTPlatePlugin` accepts an explicit `PluginConfig` generic for full type safety across options, API methods, and transforms. ```typescript import { createPlatePlugin, createTPlatePlugin } from 'platejs/react'; import type { PluginConfig } from 'platejs'; // ---- Simple plugin ---- const SavePlugin = createPlatePlugin({ key: 'save', handlers: { onChange: ({ editor, value }) => { console.info('Content changed, nodes:', value.length); }, onKeyDown: ({ editor, event }) => { if ((event.metaKey || event.ctrlKey) && event.key === 's') { event.preventDefault(); console.info('Save triggered'); } }, }, }); // ---- Fully-typed plugin with options, API, and transforms ---- type WordCountConfig = PluginConfig< 'wordCount', { limit: number }, { wordCount: { get: () => number } }, { wordCount: { check: () => void } } >; const WordCountPlugin = createTPlatePlugin({ key: 'wordCount', options: { limit: 1000 }, }) .extendEditorApi(({ editor }) => ({ wordCount: { get: () => editor.children .flatMap((n: any) => n.children ?? []) .reduce((acc: number, t: any) => acc + (t.text?.split(/\s+/).length ?? 0), 0), }, })) .extendEditorTransforms(({ editor, getOptions }) => ({ wordCount: { check: () => { const count = editor.api.wordCount.get(); if (count > getOptions().limit) { console.warn(`Word limit exceeded: ${count}/${getOptions().limit}`); } }, }, })); // Usage const editor = createPlateEditor({ plugins: [WordCountPlugin] }); const count = editor.api.wordCount.get(); // type-safe editor.tf.wordCount.check(); // type-safe ``` --- ### Plugin Methods: `.configure`, `.extend`, `.withComponent`, `.overrideEditor` These chainable methods customise existing or new plugins without altering the original. ```typescript import { createPlatePlugin } from 'platejs/react'; import { BoldPlugin, H1Plugin } from '@platejs/basic-nodes/react'; import { H1Element } from '@/components/ui/heading-node'; // .configure — modify options on an existing plugin (no type extension) const CustomBold = BoldPlugin.configure({ shortcuts: { toggle: { keys: [['Meta', 'Shift', 'b']] }, // remap default Mod+B }, }); // .extend — add new options/handlers (extends the type) const TrackedBold = BoldPlugin.extend(({ editor }) => ({ handlers: { onKeyDown: ({ event }) => { if ((event.metaKey || event.ctrlKey) && event.key === 'b') { console.info('Bold toggle via keyboard'); } }, }, })); // .withComponent — bind a React component to a plugin's node type const H1WithCustomElement = H1Plugin.withComponent(H1Element); // .overrideEditor — safely override core editor methods const LoggingPlugin = createPlatePlugin({ key: 'logging' }).overrideEditor( ({ editor, tf: { insertText } }) => ({ transforms: { insertText(text, options) { console.info('Inserting text:', text); insertText(text, options); // call original }, }, }) ); ``` --- ### Plugin Rules — declarative editing behaviours The `rules` property on a plugin replaces `ResetNodePlugin`, `SoftBreakPlugin`, and manual `overrideEditor` for common cases like Enter and Backspace behaviour inside custom blocks. ```typescript import { createPlatePlugin } from 'platejs/react'; import { BlockquotePlugin, H1Plugin } from '@platejs/basic-nodes/react'; // Press Enter in an empty heading → reset to paragraph const SmartH1 = H1Plugin.configure({ rules: { break: { empty: 'reset' }, // Enter on empty block → paragraph }, }); // Press Enter in an empty blockquote → exit the block const SmartBlockquote = BlockquotePlugin.configure({ rules: { break: { empty: 'exit' }, // Enter on empty → insert paragraph after delete: { start: 'reset' }, // Backspace at start → unwrap to paragraph }, }); // Custom structural plugin with strict sibling constraint const ColumnPlugin = createPlatePlugin({ key: 'column', node: { isElement: true, isStrictSiblings: true, isContainer: true }, rules: { normalize: { removeEmpty: true }, }, }); ``` --- ### Plugin Shortcuts Keyboard shortcuts can be linked to plugin transforms by name, or use a custom `handler`. Default shortcuts for Bold (`Mod+B`), Italic (`Mod+I`), and Underline (`Mod+U`) are built in. ```typescript import { createPlatePlugin, Key } from 'platejs/react'; export const FormattingPlugin = createPlatePlugin({ key: 'fmt' }) .extendTransforms(() => ({ applyCode: () => { /* toggle code mark logic */ }, applyStrike: () => { /* toggle strikethrough */ }, })) .extend({ shortcuts: { // Links to editor.tf.fmt.applyCode() by name applyCode: { keys: [[Key.Mod, 'e']] }, // Custom handler applyStrike: { keys: [[Key.Mod, Key.Shift, 's']], handler: ({ editor, event }) => { console.info('Strikethrough via custom handler'); }, }, }, }); // Global shortcuts on the editor itself const editor = createPlateEditor({ plugins: [FormattingPlugin], shortcuts: { saveDocument: { keys: [[Key.Mod, 's']], handler: ({ editor }) => { console.info('Saving…', editor.children); }, preventDefault: false, // allow browser Save dialog }, }, }); ``` --- ### Editor Method Hooks — `useEditorRef`, `useEditorSelector`, `useEditorState` Three hooks provide access to the `PlateEditor` inside child components with different re-render profiles. ```tsx import { useEditorRef, useEditorSelector, useEditorState, useEditorMounted, PlateController, } from 'platejs/react'; // Never re-renders — best for callbacks function SaveButton() { const editor = useEditorRef(); return ; } // Re-renders only when selection presence changes function SelectionIndicator() { const hasSelection = useEditorSelector((ed) => !!ed.selection, []); return {hasSelection ? 'Text selected' : 'No selection'}; } // Re-renders on every keystroke (use sparingly) function WordCounter() { const editor = useEditorState(); const count = editor.children .flatMap((n: any) => n.children ?? []) .reduce((acc: number, t: any) => acc + (t.text?.split(/\s+/).filter(Boolean).length ?? 0), 0); return Words: {count}; } // Manage a shared toolbar outside using PlateController function App() { return ( ); } function SharedToolbar() { const editor = useEditorState(); const mounted = useEditorMounted(); if (!mounted) return null; return ( ); } ``` --- ### `editor.tf.*` Transforms — modify editor state All state-changing operations live under `editor.tf` (alias of `editor.transforms`). Plugin transforms are namespaced by plugin key. ```tsx import { useEditorRef } from 'platejs/react'; function Toolbar() { const editor = useEditorRef(); return (
{/* Mark toggles */} {/* Block type toggles */} {/* Core transforms */}
); } ``` --- ### `editor.api.*` — Query the editor Non-mutating methods (queries, utilities) live under `editor.api`. Plugin APIs are namespaced by plugin key. ```typescript import { createPlateEditor } from 'platejs/react'; import { FindReplacePlugin } from '@platejs/find-replace'; import { TablePlugin } from '@platejs/table/react'; const editor = createPlateEditor({ plugins: [FindReplacePlugin, TablePlugin] }); // Core API const node = editor.api.node({ at: [0] }); // get node at path const isVoid = editor.api.isVoid(node?.[0] as any); // check void const block = editor.api.block({ at: editor.selection! }); // current block // Plugin API const searchText = editor.getOption(FindReplacePlugin, 'search'); editor.setOption(FindReplacePlugin, 'search', 'hello'); const tableApi = editor.getApi(TablePlugin); tableApi.api.create.tableCell?.(); // Cross-plugin typed transforms const tableTf = editor.getTransforms(TablePlugin); tableTf.insert.tableRow?.(); ``` --- ### `editor.getOption` / `editor.setOption` / `editor.setOptions` — plugin state Each plugin has its own reactive options store (powered by zustand-x). ```typescript import { createPlateEditor } from 'platejs/react'; import { FindReplacePlugin } from '@platejs/find-replace'; const editor = createPlateEditor({ plugins: [FindReplacePlugin] }); // Read const search = editor.getOption(FindReplacePlugin, 'search'); const allOpts = editor.getOptions(FindReplacePlugin); // Write — scalar editor.setOption(FindReplacePlugin, 'search', 'foo'); // Write — multiple options editor.setOptions(FindReplacePlugin, { search: 'bar', caseSensitive: true }); // Write — Immer draft editor.setOptions(FindReplacePlugin, (draft) => { draft.search = 'baz'; draft.caseSensitive = false; }); // React: subscribe in a component import { usePluginOption } from 'platejs/react'; function SearchDisplay() { const search = usePluginOption(FindReplacePlugin, 'search'); return Searching: {search}; } ``` --- ### Controlled Value — async init & external reset The recommended pattern is uncontrolled, but external updates are possible via `editor.tf.setValue` (re-renders all nodes) or `editor.tf.init` for async scenarios. ```tsx import React, { useEffect } from 'react'; import { Plate, usePlateEditor } from 'platejs/react'; import { Editor, EditorContainer } from '@/components/ui/editor'; // Async initial value function AsyncEditor() { const editor = usePlateEditor({ value: async () => { const res = await fetch('/api/document/1'); const data = await res.json(); return data.content; }, autoSelect: 'end', onReady: ({ value }) => console.info('Loaded', value.length, 'nodes'), }); return ( ); } // Manual init (full control over timing) function ManualInitEditor() { const editor = usePlateEditor({ shouldInitialize: false }); useEffect(() => { fetch('/api/doc').then(r => r.json()).then(({ content }) => { editor.tf.init({ value: content, autoSelect: 'end' }); }); }, [editor]); return ( ); } ``` --- ### Markdown Plugin — `@platejs/markdown` Bidirectional Markdown ↔ Plate conversion via `editor.api.markdown.deserialize` and `editor.api.markdown.serialize`. Supports GFM, math, emoji shortcodes, MDX, and custom element rules. ```tsx import { createPlateEditor } from 'platejs/react'; import { MarkdownPlugin, remarkMdx, remarkMention, } from '@platejs/markdown'; import remarkGfm from 'remark-gfm'; import remarkMath from 'remark-math'; import { H1Plugin, BoldPlugin, ItalicPlugin } from '@platejs/basic-nodes/react'; import { TablePlugin } from '@platejs/table/react'; const editor = createPlateEditor({ plugins: [ H1Plugin, BoldPlugin, ItalicPlugin, TablePlugin, MarkdownPlugin.configure({ options: { remarkPlugins: [remarkGfm, remarkMath, remarkMdx, remarkMention], }, }), ], // Deserialize Markdown as initial value value: (ed) => ed.getApi(MarkdownPlugin).markdown.deserialize( `# Hello\n\n**Bold** and *italic* text.\n\n| Col1 | Col2 |\n|---|---|\n| A | B |` ), }); // Serialize current content back to Markdown const markdown = editor.api.markdown.serialize(); console.info(markdown); // Output: "# Hello\n\n**Bold** and *italic* text.\n\n| Col1 | Col2 |\n|---|---|\n| A | B |" // Serialize specific nodes const partial = editor.api.markdown.serialize({ value: [{ type: 'p', children: [{ text: 'Just this.' }] }], }); ``` --- ### AI Plugin — `@platejs/ai` Streaming AI text generation with Vercel AI SDK integration. Commands insert/replace content, stream Markdown, and support undo-safe batching. ```tsx import { createPlateEditor } from 'platejs/react'; import { AIChatPlugin, AIPlugin } from '@platejs/ai/react'; import { BlockSelectionPlugin } from '@platejs/selection/react'; import { MarkdownPlugin } from '@platejs/markdown'; const editor = createPlateEditor({ plugins: [ BlockSelectionPlugin, MarkdownPlugin, AIPlugin, AIChatPlugin.configure({ options: { // Vercel AI SDK `useChat`-compatible submit function chat: { api: '/api/ai/command', // your streaming endpoint body: { model: 'gpt-4o' }, }, }, }), ], }); // Trigger AI chat with a prompt editor.api.aiChat.submit({ prompt: 'Improve the selected text.' }); // Replace selected blocks with AI output editor.tf.aiChat.replaceSelection(); // Insert AI output below current selection editor.tf.aiChat.insertBelow(); // Undo all AI changes in the current batch editor.tf.ai.undo(); ``` --- ### Table Plugin — `@platejs/table` Full-featured table editing with arrow navigation, cell/row selection, `Shift+Arrow` expansion, and drag-to-reorder rows. ```tsx import { createPlateEditor } from 'platejs/react'; import { TablePlugin } from '@platejs/table/react'; import { TableElement, TableRowElement, TableCellElement, TableCellHeaderElement, } from '@/components/ui/table-node'; const editor = createPlateEditor({ plugins: [ TablePlugin.configure({ options: { minColumnWidth: 48, disableMarginLeft: false, disableExpandOnInsert: false, }, }) .withComponent(TableElement) // applies to 'table' nodes ], }); // Programmatically insert a 3×2 table editor.tf.insert.table({ rowCount: 3, colCount: 2 }); // Insert a row after the current cell editor.tf.insert.tableRow(); // Insert a column to the right editor.tf.insert.tableColumn(); // Delete current row editor.tf.remove.tableRow(); ``` --- ### Mention Plugin — `@platejs/mention` `@`-triggered combobox that inserts inline mention elements. Trigger character, pattern, and component are all configurable. ```tsx import { createPlateEditor } from 'platejs/react'; import { MentionPlugin, MentionInputPlugin } from '@platejs/mention/react'; import { MentionElement, MentionInputElement } from '@/components/ui/mention-node'; const USERS = [ { key: 'alice', text: 'Alice' }, { key: 'bob', text: 'Bob' }, ]; const editor = createPlateEditor({ plugins: [ MentionPlugin.configure({ options: { trigger: '@', triggerPreviousCharPattern: /^$|^[\s"']$/, insertSpaceAfterMention: true, }, }).withComponent(MentionElement), MentionInputPlugin.withComponent(MentionInputElement), ], }); // Insert a mention programmatically editor.api.insert.mention({ search: 'alice', value: USERS[0] }); ``` --- ### Yjs Plugin — `@platejs/yjs` (Real-time Collaboration) Multi-provider Yjs collaboration with Hocuspocus (WebSocket) and WebRTC, remote cursor awareness, and a manual lifecycle API. ```tsx import { createPlateEditor, usePlateEditor, Plate } from 'platejs/react'; import { YjsPlugin } from '@platejs/yjs/react'; import { useEffect } from 'react'; import { RemoteCursorOverlay } from '@/components/ui/remote-cursor-overlay'; import { EditorContainer } from '@/components/ui/editor'; import { Editor } from '@/components/ui/editor'; function CollaborativeEditor({ docId, currentUser }) { const editor = usePlateEditor({ plugins: [ YjsPlugin.configure({ render: { afterEditable: RemoteCursorOverlay }, options: { cursors: { data: { name: currentUser.name, color: currentUser.color } }, providers: [ { type: 'hocuspocus', options: { name: docId, url: 'wss://my-hocuspocus.example.com' }, }, { type: 'webrtc', options: { roomName: docId }, }, ], }, }), ], skipInitialization: true, // Yjs manages document state }); useEffect(() => { editor.getApi(YjsPlugin).yjs.init({ id: docId, value: [{ type: 'p', children: [{ text: '' }] }], // initial if Y.Doc empty }); return () => editor.getApi(YjsPlugin).yjs.destroy(); }, [editor, docId]); return ( ); } ``` --- ### Static Rendering — `PlateStatic` / `createSlateEditor` Server-safe, read-only rendering for RSC, SSR, HTML export, or performance-critical static views. No browser APIs, no interactive overhead. ```tsx // app/blog/[slug]/page.tsx (Next.js RSC) import { createSlateEditor } from 'platejs'; import { PlateStatic } from 'platejs/static'; import { BaseBoldPlugin, BaseItalicPlugin, BaseH1Plugin } from '@platejs/basic-nodes'; import { BaseTablePlugin } from '@platejs/table'; export default async function BlogPost({ params }) { const { content } = await fetchDocument(params.slug); // returns Plate Value JSON const editor = createSlateEditor({ plugins: [BaseBoldPlugin, BaseItalicPlugin, BaseH1Plugin, BaseTablePlugin], value: content, }); return (
); } // HTML serialisation (also works in Node.js) import { serializeHtml } from 'platejs'; const html = await serializeHtml(editor, { convertNewLinesToHtmlBr: true, }); console.info(html); //

...

...

``` --- ### Form Integration (react-hook-form) Plate is uncontrolled by design. The recommended pattern for forms is syncing value via `onChange` or on blur. ```tsx import { useForm } from 'react-hook-form'; import type { Value } from 'platejs'; import { Plate, PlateContent, usePlateEditor } from 'platejs/react'; import { BoldPlugin } from '@platejs/basic-nodes/react'; type FormData = { title: string; body: Value }; const defaultBody: Value = [{ type: 'p', children: [{ text: '' }] }]; export function ArticleForm() { const { register, handleSubmit, setValue } = useForm({ defaultValues: { title: '', body: defaultBody }, }); const editor = usePlateEditor({ plugins: [BoldPlugin], value: defaultBody }); register('body'); const onSubmit = (data: FormData) => { console.info('Submitting article:', data.title, data.body); }; return (
setValue('body', value, { shouldDirty: true })} >
); } ``` --- ### Unit Testing — `@platejs/test-utils` JSX-based editor state fixtures allow declarative input/output testing of any plugin transform. ```typescript /** @jsx jsx */ import { jsx } from '@platejs/test-utils'; import { createPlateEditor } from 'platejs/react'; import { BoldPlugin } from '@platejs/basic-nodes/react'; import type { PlateEditor } from 'platejs/react'; jsx; // prevent tree-shaking it('should apply bold to selection', () => { const input = ( Hello world ) as any as PlateEditor; const expected = ( Hello world ) as any as PlateEditor; const editor = createPlateEditor({ plugins: [BoldPlugin], selection: input.selection, value: input.children, }); editor.tf.bold.toggle(); expect(editor.children).toEqual(expected.children); }); ``` --- ### Feature Kits — rapid multi-plugin setup Kits bundle a feature's plugins and their Plate UI components into a single array. Spread them into `plugins`. ```tsx import { createPlateEditor } from 'platejs/react'; // Kits live in your project after `npx shadcn@latest add @plate/` import { BasicBlocksKit } from '@/components/editor/plugins/basic-blocks-kit'; import { BasicMarksKit } from '@/components/editor/plugins/basic-marks-kit'; import { TableKit } from '@/components/editor/plugins/table-kit'; import { MediaKit } from '@/components/editor/plugins/media-kit'; import { MentionKit } from '@/components/editor/plugins/mention-kit'; import { AIKit } from '@/components/editor/plugins/ai-kit'; import { MarkdownKit } from '@/components/editor/plugins/markdown-kit'; const editor = createPlateEditor({ plugins: [ ...BasicBlocksKit, // paragraphs, headings, blockquotes, hr ...BasicMarksKit, // bold, italic, underline, strikethrough, code, highlight ...TableKit, // table, row, cell, header cell ...MediaKit, // image, video, audio, file, embed, placeholder ...MentionKit, // @mention with combobox ...MarkdownKit, // Markdown paste + serialization ...AIKit, // AI command menu + streaming ], }); ``` --- ## Summary Plate is the go-to framework for production-grade React rich-text editors that need deep customisation without fighting a black-box library. Its primary use cases are document editors (Notion-like), CMS content fields, collaborative writing tools, and AI-assisted editing surfaces. The plugin system means virtually any editing behaviour can be composed from small, well-typed units — marks, block types, serializers, normalizers, shortcuts, and render hooks all coexist without configuration conflicts. Teams start quickly with Feature Kits and Plate UI components, then graduate to manual plugin composition and `createTPlatePlugin` as their requirements grow. Integration patterns vary by environment. Browser-side applications use `usePlateEditor` + `` + `` (Plate UI), wiring `onChange` to persistence layers (localStorage, API, react-hook-form). Server-side and RSC work uses `createSlateEditor` + `` or `serializeHtml` for zero-browser-API rendering pipelines. Real-time collaboration layers `YjsPlugin` on top of any editor config with one provider array. AI workflows add `AIPlugin` + `AIChatPlugin` and a standard Vercel AI SDK streaming route. All patterns compose cleanly because every plugin exposes typed `editor.api.*` queries and `editor.tf.*` transforms, making the editor a single, predictable state container that any part of the application can read or write.