# Introduction Source: https://platejs.org/docs ## Registry URLs - Components index: https://platejs.org/r/registry.json - Docs index: https://platejs.org/r/registry-docs.json - Component content: https://platejs.org/r/{name} Any `` or `` in this page can be resolved at `https://platejs.org/r/{name}`. --- --- title: Introduction description: Build rich-text editors with Plate, Plate UI, AI, MCP, and shadcn/ui. --- Plate is a React framework for building rich-text editors. It gives you a headless editor runtime, composable plugins, and optional Plate UI components that you copy into your app. Start with Plate UI for a complete editor, or use the packages directly when you need a headless setup. ## Choose a Path ## What Plate Owns | Layer | What it owns | Start here | | --- | --- | --- | | `platejs` | Core editor runtime, React bindings, and editor APIs. | [Installation](/docs/installation) | | `@platejs/*` packages | Headless plugins for nodes, marks, serialization, collaboration, AI, and editor behavior. | [Plugin guide](/docs/plugin) | | Plate UI registry | App-local UI components, kits, editor templates, and API routes installed through the shadcn CLI. | [Plate UI](/docs/installation/plate-ui) | | Your app | The copied component code, styling, routing, data model, and product-specific editor behavior. | [Plugin Components](/docs/plugin-components) | ## Why Plate UI? Plate UI follows the shadcn/ui model: you copy the code, own it, and keep editing it in your app. - **Open code:** The UI layer is app-local code, not a sealed component package. - **Composition:** Components use predictable React and shadcn/ui conventions. - **CLI distribution:** Add kits, templates, components, API routes, and docs through the shadcn CLI. - **AI-ready:** Open component code and consistent registry metadata give agents real context. - **MCP-ready:** The [MCP setup](/docs/installation/mcp) lets AI tools inspect and apply Plate registry items. Done. You can install the full UI path, drop down to headless packages, or start from an editor template. ## FAQ Plate works in React environments. Use the [Next.js guide](/docs/installation/next) for server-rendered apps, the [React guide](/docs/installation/react) for Vite and client-side apps, [RSC](/docs/installation/rsc) for server components, and [Node.js](/docs/installation/node) for backend-only processing. **Plate** is the headless editor framework: runtime, plugins, transforms, APIs, and React bindings. **Plate UI** is the copied UI layer: components, kits, editor templates, and API routes installed into your app. Yes. Plate and Plate UI are free for personal and commercial projects. Attribution is not required. No. Plate UI uses [shadcn/ui](https://ui.shadcn.com/) conventions for editor-specific UI. Use shadcn/ui for the rest of your application. Package behavior updates through `platejs` and `@platejs/*` dependencies. Copied UI code is yours, so update it by comparing the Plate registry source with your local files or by using the [MCP setup](/docs/installation/mcp). The copied component layer lets you change markup, styles, toolbar behavior, and product details without waiting for a package API to expose every option. No. Plate UI is distributed as registry code through the shadcn CLI so the UI layer stays owned by your app. ## Credits - [shadcn/ui](https://ui.shadcn.com/) - For UI inspiration, documentation, and the CLI. - [Radix UI](https://radix-ui.com) - For the unstyled, accessible primitives. - [Vercel](https://vercel.com) - For hosting. - [Shu Ding](https://shud.in) - Typography style adapted from [Nextra](https://nextra.site). - [cmdk](https://cmdk.paco.me) - For the `` component. # Getting Started Source: https://platejs.org/docs/installation ## Registry URLs - Components index: https://platejs.org/r/registry.json - Docs index: https://platejs.org/r/registry-docs.json - Component content: https://platejs.org/r/{name} Any `` or `` in this page can be resolved at `https://platejs.org/r/{name}`. --- --- title: Getting Started description: Choose the right Plate installation path for your React project. --- Plate has different setup paths for UI-first editors, headless React editors, server components, and backend processing. Start with Plate UI when you want a complete editor surface. Use the lower-level guides when you need to own the rendering, runtime environment, or server-only pipeline. ## Choose a Path ### Install Plate UI Use Plate UI when you want the fastest working editor path. ```bash npx shadcn@latest add @plate/plate-ui ``` ### Pick the Runtime Guide After Plate UI is installed, choose the runtime guide that matches your app: - [Next.js](/docs/installation/next) for server-rendered React apps. - [React](/docs/installation/react) for Vite, React Router, and other client-side apps. ### Add Features Use feature kits when you want complete plugin + UI wiring. Use manual plugin docs when you want only the headless package behavior. ## Other Environments | Environment | Use it when | Guide | | --- | --- | --- | | Manual React | You do not want Plate UI or shadcn/ui. | [Manual Installation](/docs/installation/manual) | | React Server Components | You need static content generation or server-side editor processing without client-side interactivity. | [RSC](/docs/installation/rsc) | | Node.js | You need backend migration, validation, or serialization scripts. | [Node.js](/docs/installation/node) | Server-only environments use base package imports like `platejs` and `@platejs/basic-nodes`. Do not import from `/react` subpaths in RSC or Node.js. ## Next Steps Once the editor runs, use these docs to customize it: - [Editor](/docs/editor) explains the editor instance. - [Plugin](/docs/plugin) explains plugin configuration. - [Plugin Components](/docs/plugin-components) explains node and mark rendering. - [Troubleshooting](/docs/troubleshooting) covers common setup issues. Done. You now have the right install path for your app surface. # Controlled Editor Value Source: https://platejs.org/docs/controlled ## Registry URLs - Components index: https://platejs.org/r/registry.json - Docs index: https://platejs.org/r/registry-docs.json - Component content: https://platejs.org/r/{name} Any `` or `` in this page can be resolved at `https://platejs.org/r/{name}`. --- --- title: Controlled Editor Value description: Control initial values, persistence, replacement, and async initialization. --- Plate is not a normal controlled text input. The editor owns content, selection, history, plugin state, and normalization. This guide shows the safe control points: initial values, change persistence, explicit replacement, reset, and delayed initialization. ## Value Ownership Do not mirror `editor.children` into React state and pass it back on every change. That fights Slate selection/history and turns normal typing into a full-document replacement loop. | Goal | API | | --- | --- | | Set initial content. | `value` in `usePlateEditor` or `createPlateEditor`. | | Persist edits. | `` or ``. | | Replace content from outside the editor. | `editor.tf.setValue(value)`. | | Reset editor state. | `editor.tf.reset()`. | | Delay initialization. | `skipInitialization: true` plus `editor.tf.init(...)`. | ### Set the Initial Value Pass a `Value`, an HTML string, a function, or an async function to `value`. ```tsx title="components/editor.tsx" showLineNumbers import type { Value } from 'platejs'; import { Plate, usePlateEditor } from 'platejs/react'; import { Editor, EditorContainer } from '@/components/ui/editor'; const initialValue: Value = [ { children: [{ text: 'Initial value' }], type: 'p', }, ]; export function MyEditor() { const editor = usePlateEditor({ value: initialValue, }); return ( ); } ``` ### Persist Changes Use `onValueChange` when you only need the document value. ```tsx title="components/editor.tsx" showLineNumbers {15-19,25} import type { Value } from 'platejs'; import { Plate, usePlateEditor } from 'platejs/react'; import { Editor, EditorContainer } from '@/components/ui/editor'; const STORAGE_KEY = 'plate-value'; const initialValue: Value = [ { children: [{ text: 'Autosaved value' }], type: 'p', }, ]; function saveValue(value: Value) { localStorage.setItem(STORAGE_KEY, JSON.stringify(value)); } export function MyEditor() { const editor = usePlateEditor({ value: () => { const saved = localStorage.getItem(STORAGE_KEY); return saved ? JSON.parse(saved) : initialValue; }, }); return ( saveValue(value)}> ); } ``` Use `onChange` when the callback needs the editor instance too. ```tsx title="components/editor.tsx" { console.info(editor.id, value); }} /> ``` ### Replace or Reset Content Use transforms for external changes. `setValue` replaces the document and `reset` returns the editor to its initialized state. ```tsx title="components/replace-controls.tsx" showLineNumbers import type { Value } from 'platejs'; import { useEditorRef } from 'platejs/react'; import { Button } from '@/components/ui/button'; const replacementValue: Value = [ { children: [{ text: 'Replaced value' }], type: 'p', }, ]; export function ReplaceControls() { const editor = useEditorRef(); return (
); } ``` `editor.tf.setValue` replaces nodes at the document root. Use it for explicit outside-editor changes, not for every `onValueChange`. [controlled-demo registry content](https://platejs.org/r/controlled-demo) ### Load Async Initial Content Use an async `value` function when the editor can initialize as soon as the data resolves. ```tsx title="components/async-editor.tsx" showLineNumbers import { Plate, usePlateEditor } from 'platejs/react'; import { Editor, EditorContainer } from '@/components/ui/editor'; export function AsyncEditor() { const editor = usePlateEditor({ autoSelect: 'end', value: async () => { const response = await fetch('/api/document'); const data = await response.json(); return data.content; }, onReady: ({ isAsync, value }) => { if (isAsync) console.info('Loaded value:', value); }, }); return ( ); } ``` ### Initialize Manually Use `skipInitialization` when another system owns the startup moment, such as collaboration or a multi-step loader. ```tsx title="components/manual-init-editor.tsx" showLineNumbers {8,13-18} import * as React from 'react'; import { Plate, usePlateEditor } from 'platejs/react'; import { Editor, EditorContainer } from '@/components/ui/editor'; export function ManualInitEditor() { const editor = usePlateEditor({ skipInitialization: true, }); React.useEffect(() => { void fetch('/api/document') .then((response) => response.json()) .then((data) => { editor.tf.init({ autoSelect: 'end', value: data.content, }); }); }, [editor]); return ( ); } ```
Done. Plate owns live editor state; your app controls the entry points around it. # Debugging Source: https://platejs.org/docs/debugging ## Registry URLs - Components index: https://platejs.org/r/registry.json - Docs index: https://platejs.org/r/registry-docs.json - Component content: https://platejs.org/r/{name} Any `` or `` in this page can be resolved at `https://platejs.org/r/{name}`. --- --- title: Debugging description: Debugging in Plate. --- ## Using the DebugPlugin The `DebugPlugin` is automatically included when you create a Plate editor. You can access its methods through the editor's API: ```ts const editor = createPlateEditor({ plugins: [/* your plugins */], }); editor.api.debug.log('This is a log message'); editor.api.debug.info('This is an info message'); editor.api.debug.warn('This is a warning'); editor.api.debug.error('This is an error'); ``` ### Log Levels The `DebugPlugin` supports four log levels: 1. `log`: For general logging 2. `info`: For informational messages 3. `warn`: For warnings 4. `error`: For errors You can set the minimum log level to control which messages are displayed: ```ts const editor = createPlateEditor({ plugins: [ DebugPlugin.configure({ options: { logLevel: 'warn', // Only show warnings and errors }, }), ], }); ``` ### Configuration Options The `DebugPlugin` can be configured with the following options: - `isProduction`: Set to `true` to disable logging in production environments. - `logLevel`: Set the minimum log level (`'error'`, `'warn'`, `'info'`, or `'log'`). - `logger`: Provide custom logging functions for each log level. - `throwErrors`: Set to `true` to throw errors instead of logging them (default: `true`). Example configuration: ```ts const editor = createPlateEditor({ plugins: [ DebugPlugin.configure({ options: { isProduction: process.env.NODE_ENV === 'production', logLevel: 'info', logger: { error: (message, type, details) => { // Custom error logging console.error(`Custom Error: ${message}`, type, details); }, // ... custom loggers for other levels }, throwErrors: false, }, }), ], }); ``` ### Error Handling By default, the `DebugPlugin` throws errors when `error` is called. You can catch these errors and handle them as needed: ```ts try { editor.api.debug.error('An error occurred', 'CUSTOM_ERROR', { details: 'Additional information' }); } catch (error) { if (error instanceof PlateError) { console.debug(error.type); // 'CUSTOM_ERROR' console.debug(error.message); // '[CUSTOM_ERROR] An error occurred' } } ``` To log errors instead of throwing them, set `throwErrors` to `false` in the configuration. ### Best Practices 1. Use appropriate log levels for different types of messages. 2. In production, set `isProduction` to `true` to disable non-essential logging. 3. Use custom loggers to integrate with your preferred logging service. 4. Include relevant details when logging to make debugging easier. 5. Use error types to categorize and handle different error scenarios. ## Additional Debugging Strategies Besides using the DebugPlugin, there are other effective ways to debug your Plate editor: ### 1. Override Editor Methods with Logging You can use the `extendEditor` option to override editor methods and add logging: ```ts const LoggingPlugin = createPlatePlugin({ key: 'logging', }).overrideEditor(({ editor, tf: { apply } }) => ({ transforms: { apply(operation) { console.debug('Operation:', operation); apply(operation); }, }, })); const editor = createPlateEditor({ plugins: [LoggingPlugin], }); ``` This approach allows you to log operations, selections, or any other editor behavior you want to inspect. ### 2. Remove Suspected Plugins If you're experiencing issues, try removing plugins one by one to isolate the problem: ```ts const editor = createPlateEditor({ plugins: [ // Comment out or remove suspected plugins // HeadingPlugin, // BoldPlugin, // ...other plugins ], }); ``` Gradually add plugins back until you identify the one causing the issue. ### 3. Use React DevTools React DevTools can be invaluable for debugging Plate components: 1. Install the React DevTools browser extension. 2. Open your app and the DevTools. 3. Navigate to the Components tab. 4. Inspect Plate components, their props, and state. ### 4. Use Browser DevTools Breakpoints Set breakpoints in your code using browser DevTools: 1. Open your app in the browser and open DevTools. 2. Navigate to the Sources tab. 3. Find your source file and click on the line number where you want to set a breakpoint. 4. Interact with your editor to trigger the breakpoint. 5. Inspect variables and step through the code. ### 5. Create Minimal Reproducible Examples If you're facing a complex issue: 1. Pick a [template](/docs/installation). 2. Add only the essential plugins and components to reproduce the issue. 3. If the issue persists, [open an issue on GitHub](https://github.com/udecode/plate/issues/new?assignees=&labels=bug&projects=&template=bug.yml) or share your example on [Discord](https://discord.gg/mAZRuBzGM3). ### 6. Use Redux DevTools for zustand stores Zustand and thus zustand-x works with the Redux DevTools browser extension. It can be very useful to help track state changes in zustand stores. Follow the [zustand documentation](https://zustand.docs.pmnd.rs/middlewares/devtools) to get going with Redux DevTools and zustand. ## Debug Error Types Plate uses several predefined error types to help identify specific issues during development. Here's a list of these error types and their descriptions: ### DEFAULT A general error that doesn't fit into other specific categories. Used when no other error type is applicable to the situation. ### OPTION_UNDEFINED Thrown when an attempt is made to access an undefined plugin option. This occurs when trying to use a plugin option that hasn't been set or is undefined. ### OVERRIDE_MISSING Indicates that an expected override is missing in a plugin configuration. This happens when a plugin expects certain overrides to be provided, but they are not present in the configuration. ### PLUGIN_DEPENDENCY_MISSING Occurs when a required plugin dependency is not found. This error is thrown when a plugin depends on another plugin that hasn't been registered or included in the editor configuration. ### PLUGIN_MISSING Indicates an attempt to use a plugin that hasn't been registered. This happens when trying to access or use a plugin that is not part of the current editor configuration. ### USE_CREATE_PLUGIN Thrown when a plugin wasn't created using `createSlatePlugin` or `createPlatePlugin` function. This error occurs when a plugin is added to the editor without being properly created using the designated function. ### USE_ELEMENT_CONTEXT Indicates that the `useElement` hook is being used outside of the appropriate element context. This occurs when trying to access element-specific data or functionality outside of the correct component context. ### PLUGIN_NODE_TYPE Thrown when a plugin is incorrectly configured as both an element and a leaf. This error occurs when a plugin's configuration contradicts itself by setting both `isElement` and `isLeaf` to true. # Editing Behavior Source: https://platejs.org/docs/editing-behavior ## Registry URLs - Components index: https://platejs.org/r/registry.json - Docs index: https://platejs.org/r/registry-docs.json - Component content: https://platejs.org/r/{name} Any `` or `` in this page can be resolved at `https://platejs.org/r/{name}`. --- --- title: Editing Behavior description: How Plate handles Enter, Backspace, merge, normalize, and selection behavior. --- Editing behavior is the path from a key press or transform to the final document shape. Use [Plugin Rules](/docs/plugin-rules) for declarative node policy, and use [Editor Methods](/docs/editor-methods) when you need an imperative transform. This guide shows how break, delete, merge, normalize, and selection behavior fit together. ## On This Page - [Choose the Right Surface](#choose-the-right-surface) - [Runtime Pipeline](#runtime-pipeline) - [Break Behavior](#break-behavior) - [Delete Behavior](#delete-behavior) - [Merge Behavior](#merge-behavior) - [Normalize Behavior](#normalize-behavior) - [Selection Behavior](#selection-behavior) - [Recipes](#recipes) - [API Reference](#api-reference) ## Choose the Right Surface Most editing behavior belongs in `plugin.rules`. Reach for custom transforms only when the rule table cannot express the behavior. | Need | Use | | --- | --- | | Change how `Enter` works in a node. | `rules.break` | | Change how `Backspace` works at the start of a block. | `rules.delete` | | Decide whether an empty sibling disappears during a merge. | `rules.merge` | | Remove empty nodes during normalization. | `rules.normalize` | | Control mark or inline boundaries while typing and moving. | `rules.selection` | | Apply one plugin's rules to another node type. | `rules.match` | | Run product-specific behavior that rules cannot express. | `.overrideEditor()` or an explicit `editor.tf.*` command | Rules keep common editor policy close to the plugin that owns the node. Transforms are still the right tool for commands, toolbar actions, and behavior that depends on app state. ## Runtime Pipeline Plate resolves plugins first, then core plugins wrap Slate APIs and transforms. | Layer | Owner | Handles | | --- | --- | --- | | `OverridePlugin` | Core runtime | Node flags, break rules, delete rules, merge rules, normalize rules. | | `AffinityPlugin` | Core runtime | `rules.selection` for mark and inline boundaries. | | Feature plugins | Feature packages | Default rules for headings, callouts, lists, links, tables, marks, and other nodes. | | App plugins | Your app | Local overrides, custom node policy, and custom transforms. | The normal flow is: ```txt key press or command -> optional input rule for typed patterns -> plugin rule lookup for the current node -> editor.tf transform -> merge guard when nodes are joined -> normalization -> selection affinity cleanup ``` Input rules are for text patterns such as markdown shortcuts and autolinks. Plugin rules are for node behavior such as "a heading resets to paragraph on Backspace" or "a callout inserts soft breaks on Enter." ## Break Behavior `rules.break` controls `editor.tf.insertBreak()`, which is what `Enter` calls. Plate checks the current block and handles these cases in order: | Case | Rule | What happens | | --- | --- | --- | | Empty collapsed block | `break.empty` | Runs `reset`, `exit`, `lift`, `deleteExit`, or falls through. | | Cursor after a trailing newline | `break.emptyLineEnd` | Runs `exit`, `deleteExit`, or falls through. | | Normal Enter | `break.default` | Runs `lineBreak`, `exit`, `deleteExit`, or falls through. | | Split created a new block | `break.splitReset` | Resets the new block to the default type. | Use `splitReset` for blocks that should not keep their type after a normal split. ```tsx title="plugins.tsx" showLineNumbers import { H1Plugin } from '@platejs/basic-nodes/react'; export const AppH1Plugin = H1Plugin.configure({ rules: { break: { splitReset: true, }, }, }); ``` Use `lineBreak` and `deleteExit` for container-like blocks that need soft lines before they leave the block. ```tsx title="plugins.tsx" showLineNumbers import { CalloutPlugin } from '@platejs/callout/react'; export const AppCalloutPlugin = CalloutPlugin.configure({ rules: { break: { default: 'lineBreak', empty: 'reset', emptyLineEnd: 'deleteExit', }, }, }); ``` That callout keeps normal Enter inside the callout, resets empty callouts to paragraphs, and exits after a trailing empty line. ## Delete Behavior `rules.delete` controls collapsed `Backspace` behavior. Expanded selections still delete through the normal fragment path unless the whole editor is selected. Plate checks collapsed `deleteBackward` in this order: | Case | Rule | What happens | | --- | --- | --- | | Cursor at the start of the current block | `delete.start` | Runs `reset`, `lift`, or falls through. | | Current block is empty | `delete.empty` | Runs `reset` or falls through. | | Cursor is at the start of the document | Core default | Resets the first block. | | Nothing handled the case | Slate transform | Runs the original delete transform. | Use `start: 'reset'` for formatted text blocks that should become paragraphs before they merge into the previous block. ```tsx title="plugins.tsx" showLineNumbers import { H1Plugin } from '@platejs/basic-nodes/react'; export const AppH1Plugin = H1Plugin.configure({ rules: { delete: { start: 'reset', }, }, }); ``` Use `start: 'lift'` for nested blocks that should move out one ancestor level. ```tsx title="plugins.tsx" showLineNumbers import { createPlatePlugin } from 'platejs/react'; export const QuoteItemPlugin = createPlatePlugin({ key: 'quote_item', node: { isElement: true, }, rules: { delete: { start: 'lift', }, }, }); ``` When a selection spans multiple blocks, Plate deletes the selected content and then calls `editor.tf.mergeNodes()` at the end boundary. That means cross-block deletion can still enter the merge pipeline below. ## Merge Behavior Merge behavior decides whether two nodes can join and whether empty nodes at the boundary disappear. `editor.tf.mergeNodes()` calls `editor.api.shouldMergeNodes(prev, next, options)` before it applies the merge. Plate's merge rules add three important guards: | Case | Behavior | | --- | --- | | Empty text node before the merge point | Remove it when it is not the first child. | | Empty previous sibling | Remove it only when the owning plugin has `rules.merge.removeEmpty: true`. | | Target node is void | Do not delete the void target by default; remove the current empty node instead when possible. | Use `removeEmpty: true` for text-like blocks such as paragraphs and headings. ```tsx title="plugins.tsx" showLineNumbers import { H1Plugin } from '@platejs/basic-nodes/react'; export const AppH1Plugin = H1Plugin.configure({ rules: { merge: { removeEmpty: true, }, }, }); ``` Keep `removeEmpty: false` for structural nodes that own layout, children, or wrappers. Tables, rows, cells, columns, and callouts should not disappear just because a merge crosses their boundary. ```tsx title="plugins.tsx" showLineNumbers import { CalloutPlugin } from '@platejs/callout/react'; export const StableCalloutPlugin = CalloutPlugin.configure({ rules: { merge: { removeEmpty: false, }, }, }); ``` Merge rules are not table cell merge commands. `rules.merge` protects document structure during node joins. Table cell merge and split commands live on `editor.tf.table.merge()` and `editor.tf.table.split()`. ## Normalize Behavior `rules.normalize` runs during Slate normalization. Use `normalize.removeEmpty` for elements that should not exist without text content. Links use this shape because an empty link has no useful editing surface. ```tsx title="plugins.tsx" showLineNumbers import { LinkPlugin } from '@platejs/link/react'; export const AppLinkPlugin = LinkPlugin.configure({ rules: { normalize: { removeEmpty: true, }, }, }); ``` Keep normalization rules boring. If a node needs a rich repair strategy, write a dedicated normalizer with `.overrideEditor()` so the behavior is explicit and testable. ## Selection Behavior `rules.selection` controls how marks and inline-like boundaries behave while typing, deleting, and moving the cursor. | Affinity | Use for | | --- | --- | | `default` | Normal Slate boundary behavior. | | `directional` | Links and highlights where cursor direction decides whether typed text stays inside. | | `outward` | Comment and suggestion marks where edge typing should leave the mark. | | `hard` | Boundaries that should take an extra arrow-key step to cross. | Node flags also affect editing behavior. `node.isInline`, `node.isVoid`, `node.isSelectable`, and `node.isMarkableVoid` are resolved by the core override layer before selection and transform logic runs. ## Recipes | Behavior | Configure | | --- | --- | | Heading resets to paragraph on Backspace. | `rules.delete.start: 'reset'` | | Heading splits into paragraph on Enter. | `rules.break.splitReset: true` | | Callout keeps Enter inside the block. | `rules.break.default: 'lineBreak'` | | Empty callout becomes a paragraph. | `rules.break.empty: 'reset'` or `rules.delete.start: 'reset'` | | Nested item outdents on Backspace. | `rules.delete.start: 'lift'` | | Empty text block disappears during merge. | `rules.merge.removeEmpty: true` | | Structural wrapper survives merge. | `rules.merge.removeEmpty: false` | | Empty inline element disappears. | `rules.normalize.removeEmpty: true` | | Link boundary follows cursor direction. | `rules.selection.affinity: 'directional'` | For list metadata and code-block children, use `rules.match` so the feature plugin can apply its rule to the child block that actually contains the selection. ## API Reference | Surface | Owner | Reference | | --- | --- | --- | | `rules.break` | Core rule engine plus feature plugin config | [Plugin Rules](/docs/plugin-rules#rulesbreak) | | `rules.delete` | Core rule engine plus feature plugin config | [Plugin Rules](/docs/plugin-rules#rulesdelete) | | `rules.merge` | Core rule engine plus feature plugin config | [Plugin Rules](/docs/plugin-rules#rulesmerge) | | `rules.normalize` | Core rule engine plus feature plugin config | [Plugin Rules](/docs/plugin-rules#rulesnormalize) | | `rules.selection` | Affinity core plugin plus feature plugin config | [Plugin Rules](/docs/plugin-rules#rulesselection) | | `rules.match` | Core rule lookup plus feature plugin config | [Plugin Rules](/docs/plugin-rules#rulesmatch) | | `editor.tf.mergeNodes()` | Slate transform patched by Plate | [Editor Transforms](/docs/api/slate/editor-transforms#mergenodes) | | `editor.api.shouldMergeNodes()` | Slate editor API patched by Plate | [Editor API](/docs/api/slate/editor-api#shouldmergenodes) | | `editor.tf.table.merge()` | Table feature package | [Table](/docs/table#editing-behavior) | Done. Rules describe the policy; transforms do the work. # Editor Methods Source: https://platejs.org/docs/editor-methods ## Registry URLs - Components index: https://platejs.org/r/registry.json - Docs index: https://platejs.org/r/registry-docs.json - Component content: https://platejs.org/r/{name} Any `` or `` in this page can be resolved at `https://platejs.org/r/{name}`. --- --- title: Editor Methods description: Read, mutate, and configure a Plate editor instance. --- The Plate editor exposes two main method surfaces: `editor.api` for reads and helpers, and `editor.tf` for transforms that change editor state. In React, pick the editor hook by how often the component should re-render. ## Access the Editor | Need | Use | | --- | --- | | Read the editor inside callbacks without re-rendering. | `useEditorRef()` | | Re-render from one derived value. | `useEditorSelector(selector, deps)` | | Re-render on every editor change. | `useEditorState()` | | Reach the active editor from outside a `` subtree. | `` plus the same hooks. | ```tsx title="components/bold-button.tsx" showLineNumbers import { useEditorRef, useEditorSelector } from 'platejs/react'; import { Button } from '@/components/ui/button'; export function BoldButton() { const editor = useEditorRef(); const hasSelection = useEditorSelector( (editor) => Boolean(editor.selection), [] ); return ( ); } ``` ### `useEditorRef` `useEditorRef` returns the stable editor object. Use it for event handlers, effects, commands, and reads that should not cause a render. ```tsx title="components/insert-paragraph-button.tsx" const editor = useEditorRef(); editor.tf.insertNodes({ children: [{ text: 'Inserted paragraph' }], type: 'p', }); ``` ### `useEditorSelector` `useEditorSelector` subscribes to a derived value. Return a primitive or provide `equalityFn` when the selected value needs custom comparison. ```tsx title="components/selection-state.tsx" const isSelectionExpanded = useEditorSelector( (editor) => editor.api.isExpanded(), [] ); ``` ### `useEditorState` `useEditorState` subscribes to the whole editor state. Use it only when the UI really needs to update on every editor change. ```tsx title="components/selection-debug.tsx" const editor = useEditorState(); return
{JSON.stringify(editor.selection, null, 2)}
; ``` ## Outside Plate Wrap shared UI in `PlateController` when a toolbar, side panel, or inspector lives outside a single `` tree. ```tsx title="components/editor-shell.tsx" showLineNumbers import type React from 'react'; import { PlateController, useEditorMounted, useEditorRef } from 'platejs/react'; import { Button } from '@/components/ui/button'; export function EditorShell({ children }: { children: React.ReactNode }) { return ( {children} ); } function ActiveEditorToolbar() { const editor = useEditorRef(); const mounted = useEditorMounted(); if (!mounted || editor.meta.isFallback) return null; return ( ); } ``` `PlateController` resolves an editor by explicit `id`, focused editor, then primary editors. If a controller exists but no editor store is ready, hooks return a fallback editor; check `useEditorMounted()` or `!editor.meta.isFallback` before running transforms. ## Editor API and Transforms Use `editor.api` for queries and DOM/editor helpers. Use `editor.tf` for operations that change the document, selection, history, focus, or plugin state. `editor.transforms` is an alias for `editor.tf`. ```ts title="editor-methods.ts" showLineNumbers const selectedText = editor.selection ? editor.api.string(editor.selection) : ''; const currentBlock = editor.api.block(); if (selectedText && !editor.api.hasMark('bold')) { editor.tf.toggleMark('bold'); } editor.tf.insertNodes({ children: [{ text: 'New paragraph' }], type: 'p', }); ``` | Surface | Examples | Use for | | --- | --- | --- | | `editor.api` | `string`, `block`, `findPath`, `hasMark`, `isExpanded` | Reading editor state, resolving paths, checking selection. | | `editor.tf` | `insertNodes`, `setNodes`, `toggleMark`, `focus`, `reset` | Mutating document or editor state. | | `editor.transforms` | Same as `editor.tf` | Compatibility alias. Prefer `editor.tf` in new code. | ## Plugin Methods Plugin helpers let TypeScript infer APIs and transforms from the plugin you pass in. ```ts title="table-methods.ts" showLineNumbers import { TablePlugin } from '@platejs/table/react'; const api = editor.getApi(TablePlugin); const tf = editor.getTransforms(TablePlugin); const cell = api.create.tableCell(); tf.insert.tableRow(); ``` | Method | Use for | | --- | --- | | `editor.getApi(plugin)` | Typed plugin APIs merged onto `editor.api`. | | `editor.getTransforms(plugin)` | Typed plugin transforms merged onto `editor.tf`. | | `editor.getPlugin(plugin)` | The resolved plugin instance for a key or plugin object. | | `editor.getType(pluginKey)` | The node type registered for a plugin key. | | `editor.getInjectProps(plugin)` | Resolved injected node props for a plugin. | ## Plugin Options Use editor option methods when imperative code needs to read or write plugin configuration. Use `usePluginOption` or `usePluginOptions` when React UI needs to re-render from option state. ```tsx title="components/find-replace-control.tsx" showLineNumbers import { FindReplacePlugin } from '@platejs/find-replace'; import { useEditorRef, usePluginOption } from 'platejs/react'; export function FindReplaceControl() { const editor = useEditorRef(); const search = usePluginOption(FindReplacePlugin, 'search'); return ( { editor.setOption(FindReplacePlugin, 'search', event.target.value); }} /> ); } ``` ```ts title="plugin-options.ts" const options = editor.getOptions(FindReplacePlugin); editor.setOptions(FindReplacePlugin, { search: 'Plate', }); editor.setOptions(FindReplacePlugin, (draft) => { draft.search = draft.search.trim(); }); ``` | Method | Use for | | --- | --- | | `editor.getOption(plugin, key, ...args)` | One option or selector value. | | `editor.getOptions(plugin)` | Current option state for a plugin. | | `editor.setOption(plugin, key, value)` | One option update. | | `editor.setOptions(plugin, partial)` | Merge multiple option fields. | | `editor.setOptions(plugin, updater)` | Mutate option state through a draft updater. | | `editor.getOptionsStore(plugin)` | Low-level access to the plugin options store. | ## Next Steps | Task | Guide | | --- | --- | | Configure editor creation. | [Editor Configuration](/docs/editor) | | Add plugin APIs and transforms. | [Plugin Methods](/docs/plugin-methods) | | Read plugin context inside components. | [Plugin Context](/docs/plugin-context) | | Browse editor query contracts. | [Editor API](/docs/api/slate/editor-api) | | Browse editor transform contracts. | [Editor Transforms](/docs/api/slate/editor-transforms) | # Editor Configuration Source: https://platejs.org/docs/editor ## Registry URLs - Components index: https://platejs.org/r/registry.json - Docs index: https://platejs.org/r/registry-docs.json - Component content: https://platejs.org/r/{name} Any `` or `` in this page can be resolved at `https://platejs.org/r/{name}`. --- --- title: Editor Configuration description: Learn how to configure and customize the Plate editor. --- This guide covers the configuration options for the Plate editor, including basic setup, plugin management, and advanced configuration techniques. ## Basic Editor Configuration To create a basic Plate editor, you can use the `createPlateEditor` function, or `usePlateEditor` in a React component: ```ts import { createPlateEditor } from 'platejs/react'; const editor = createPlateEditor({ plugins: [HeadingPlugin], }); ``` ### Initial Value Set the initial content of the editor: ```ts const editor = createPlateEditor({ value: [ { type: 'p', children: [{ text: 'Hello, Plate!' }], }, ], }); ``` You can also initialize the editor with an HTML string and the associated plugins: ```ts const editor = createPlateEditor({ plugins: [BoldPlugin, ItalicPlugin], value: '

This is bold and italic text!

', }); ``` For a comprehensive list of plugins that support HTML string deserialization, refer to the [Plugin Deserialization Rules](/docs/html#plugin-deserialization-rules) section. ### Async Initial Value If you need to fetch the initial value asynchronously (e.g., from an API), you can pass an async function directly to the `value` option: ```tsx function AsyncEditor() { const editor = usePlateEditor({ value: async () => { // Simulate fetching data from an API const response = await fetch('/api/document'); const data = await response.json(); return data.content; }, autoSelect: 'end', onReady: ({ editor, value }) => { console.info('Editor ready with loaded value:', value); }, }); if (!editor.children.length) return
Loading…
; return ( ); } ``` ### Adding Plugins You can add plugins to your editor by including them in the `plugins` array: ```ts const editor = createPlateEditor({ plugins: [HeadingPlugin, ListPlugin], }); ``` ### Max Length Set the maximum length of the editor: ```ts const editor = createPlateEditor({ maxLength: 100, }); ``` ## Advanced Configuration ### Editor ID Set a custom id for the editor: ```ts const editor = createPlateEditor({ id: 'my-custom-editor-id', }); ``` If defined, you should always pass the `id` as the first argument in any editor retrieval methods. ### Node ID Plate includes a built-in system for automatically assigning unique IDs to nodes, which is crucial for certain plugins and for data persistence strategies that rely on stable identifiers. This feature is enabled by default. You can customize its behavior or disable it entirely through the `nodeId` option. #### Configuration To configure Node ID behavior, pass an object to the `nodeId` property when creating your editor: ```ts const editor = usePlateEditor({ // ... other plugins and options nodeId: { // Function to generate IDs (default: nanoid(10)) idCreator: () => uuidv4(), // Exclude inline elements from getting IDs (default: true) filterInline: true, // Exclude text nodes from getting IDs (default: true) filterText: true, // Reuse IDs on undo/redo and copy/paste if not in document (default: false) // Set to true if IDs should be stable across such operations. reuseId: false, // Control initial-value ID assignment (default: 'if-needed') // Use 'always' to fill every missing ID in the initial value. initialValueIds: 'always', // Prevent overriding IDs when inserting nodes with an existing id (default: false) disableInsertOverrides: false, // Only allow specific node types to receive IDs (default: all) allow: ['p', 'h1'], // Exclude specific node types from receiving IDs (default: []) exclude: ['code_block'], // Custom filter function to determine if a node should get an ID filter: ([node, path]) => { // Example: Only ID on top-level blocks return path.length === 1; }, }, }); ``` The `NodeIdPlugin` (which handles this) is part of the core plugins and is automatically included. You only need to specify the `nodeId` option if you want to customize its default behavior. #### Disabling Node IDs If you don't need automatic node IDs, you can disable the feature: ```ts const editor = usePlateEditor({ // ... other plugins and options nodeId: false, // This will disable the NodeIdPlugin }); ``` By disabling this, certain plugins that rely on node IDs will not function properly. The following plugins require block IDs to work: - **[Block Selection](/docs/block-selection)** - Needs IDs to track which blocks are selected - **[Block Menu](/docs/block-menu)** - Requires IDs to show context menus for specific blocks - **[Drag & Drop](/docs/dnd)** - Uses IDs to identify blocks during drag operations - **[Table](/docs/table)** - Relies on IDs for cell selection - **[Table of Contents](/docs/toc)** - Needs heading IDs for navigation and scrolling - **[Toggle](/docs/toggle)** - Uses IDs to track which toggles are open/closed ### Navigation Feedback Plate also includes a built-in navigation feedback plugin for "you landed here" UX after TOC jumps, footnote navigation, search jumps, and custom outline movement. This feature is enabled by default. You only need to touch the `navigationFeedback` option when you want to change the flash duration or turn the plugin off. #### Configuration ```ts const editor = createPlateEditor({ navigationFeedback: { duration: 1200, }, }); ``` The `NavigationFeedbackPlugin` is part of the core plugins and is automatically included. Use `navigationFeedback` for normal configuration and keep `rootPlugin.configurePlugin(...)` for advanced escape-hatch work. #### Disabling Navigation Feedback ```ts const editor = createPlateEditor({ navigationFeedback: false, }); ``` ### Normalization Control whether the editor should normalize its content on initialization: ```ts const editor = createPlateEditor({ shouldNormalizeEditor: true, }); ``` Note that normalization may take a few dozen milliseconds for large documents, such as the playground value. ### Auto-selection Configure the editor to automatically select a range: ```ts const editor = createPlateEditor({ autoSelect: 'end', // or 'start', or true }); ``` This is not the same as auto-focus: you can select text without focusing the editor. ### Component Overrides Override default components for plugins: ```ts const editor = createPlateEditor({ plugins: [HeadingPlugin], components: { [ParagraphPlugin.key]: CustomParagraphComponent, [HeadingPlugin.key]: CustomHeadingComponent, }, }); ``` ### Plugin Overrides Override specific plugin configurations: ```ts const editor = createPlateEditor({ plugins: [HeadingPlugin], override: { plugins: { [ParagraphPlugin.key]: { options: { customOption: true, }, }, }, }, }); ``` ### Disable Plugins Disable specific plugins: ```ts const editor = createPlateEditor({ plugins: [HeadingPlugin, ListPlugin], override: { enabled: { [HistoryPlugin.key]: false, }, }, }); ``` ### Overriding Plugins You can override core plugins or previously defined plugins by adding a plugin with the same key. The last plugin with a given key wins: ```ts const CustomParagraphPlugin = createPlatePlugin({ key: 'p', // Custom implementation }); const editor = createPlateEditor({ plugins: [CustomParagraphPlugin], }); ``` ### Root Plugin From the root plugin, you can configure any plugin: ```ts const editor = createPlateEditor({ plugins: [HeadingPlugin], rootPlugin: (plugin) => plugin.configurePlugin(LengthPlugin, { options: { maxLength: 100, }, }), }); ``` ## Typed Editor `createPlateEditor` will automatically infer the types for your editor from the value and the plugins you pass in. For explicit type creation, use the generics: ### Plugins Type ```ts const editor = createPlateEditor({ plugins: [TablePlugin, LinkPlugin], }); // Usage editor.tf.insert.tableRow() ``` ### Value Type For more complex editors, you can define your types in a separate file (e.g., `plate-types.ts`): ```ts import type { TElement, TText } from 'platejs'; import type { TPlateEditor } from 'platejs/react'; // Define custom element types interface ParagraphElement extends TElement { align?: 'left' | 'center' | 'right' | 'justify'; children: RichText[]; type: typeof ParagraphPlugin.key; } interface ImageElement extends TElement { children: [{ text: '' }] type: typeof ImagePlugin.key; url: string; } // Define custom text types interface FormattedText extends TText { bold?: boolean; italic?: boolean; } export type MyRootBlock = ParagraphElement | ImageElement; // Define the editor's value type export type MyValue = MyRootBlock[]; // Define the custom editor type export type MyEditor = TPlateEditor; export const useMyEditorRef = () => useEditorRef(); // Usage const value: MyValue = [{ type: 'p', children: [{ text: 'Hello, Plate!' }], }] const editorInferred = createPlateEditor({ plugins: [TablePlugin, LinkPlugin], value, }); // or const editorExplicit = createPlateEditor({ plugins: [TablePlugin, LinkPlugin], value, }); ``` # Feature Kits Source: https://platejs.org/docs/feature-kits ## Registry URLs - Components index: https://platejs.org/r/registry.json - Docs index: https://platejs.org/r/registry-docs.json - Component content: https://platejs.org/r/{name} Any `` or `` in this page can be resolved at `https://platejs.org/r/{name}`. --- --- title: Feature Kits description: Use registry kits to add groups of Plate plugins and UI wiring. --- Feature kits are app-owned registry files that group related Plate plugins, components, shortcuts, input rules, and helper UI. Use them to start from working Plate UI wiring, then edit the installed kit when your app needs different behavior. ## Kit Types | Kit type | Example | Use for | | --- | --- | --- | | Client/UI kit | `basic-nodes-kit`, `table-kit`, `media-kit` | Editable React editors with Plate UI components. | | Base kit | `basic-blocks-base-kit`, `table-base-kit` | Static rendering and server-safe editor setup. | | Full editor kit | `editor-kit` | A complete client editor feature stack. | | Full base kit | `editor-base-kit` | A broad static/base plugin stack. | Feature kits live under `components/editor/plugins` after installation. Full editor kits like `editor-kit` and `editor-base-kit` live under `components/editor`. They are normal TypeScript files in your app, not package exports locked inside `node_modules`. ## Use a Kit Install the kit through the Plate registry, then import it from your app. ```tsx title="components/editor.tsx" showLineNumbers import { Plate, usePlateEditor } from 'platejs/react'; import { BasicNodesKit } from '@/components/editor/plugins/basic-nodes-kit'; import { TableKit } from '@/components/editor/plugins/table-kit'; import { Editor, EditorContainer } from '@/components/ui/editor'; export function MyEditor() { const editor = usePlateEditor({ plugins: [...BasicNodesKit, ...TableKit], }); return ( ); } ``` `BasicNodesKit` composes `BasicBlocksKit` and `BasicMarksKit`. For example, `BasicBlocksKit` wires paragraph, headings, blockquote, and horizontal rule plugins to their Plate UI components; `BasicMarksKit` wires marks, input rules, shortcuts, and mark leaf components. ## Choose the Right Kit | Need | Start with | | --- | --- | | Paragraphs, headings, blockquotes, marks. | `basic-nodes-kit` | | Tables with table UI components. | `table-kit` | | Images, video, audio, files, embeds, captions. | `media-kit` | | Comments and discussions. | `comment-kit` plus `discussion-kit` | | AI menu, AI nodes, cursor overlay, and Markdown support. | `ai-kit` | | Full editable editor stack. | `editor-kit` | | Static or server-rendered output. | `editor-base-kit` or focused `*-base-kit` files | Plugin pages show the recommended kit in their installation section. Use the kit source when you need the exact plugin list, imported UI components, shortcuts, or registry dependencies. ## Base Kits Base kits use base plugins and static components. They are the right starting point for [Static Rendering](/docs/static), [Node.js](/docs/installation/node), and other non-editable pipelines. ```ts title="static-editor.ts" import { BaseEditorKit } from '@/components/editor/editor-base-kit'; import { createStaticEditor } from 'platejs/static'; const editor = createStaticEditor({ plugins: BaseEditorKit, }); ``` Use client kits in editable React editors. Use base kits when React editor hooks, editable components, floating UI, or browser-only behavior should stay out of the runtime. ## Customize a Kit Because kits are copied into your app, the cleanest customization is usually to edit the installed kit file. ```tsx title="components/editor/plugins/my-basic-kit.tsx" import { H1Plugin } from '@platejs/basic-nodes/react'; import { ParagraphPlugin } from 'platejs/react'; import { H1Element } from '@/components/ui/heading-node'; import { ParagraphElement } from '@/components/ui/paragraph-node'; export const MyBasicKit = [ ParagraphPlugin.withComponent(ParagraphElement), H1Plugin.configure({ node: { component: H1Element, }, shortcuts: { toggle: { keys: 'mod+alt+1' }, }, }), ]; ``` Use manual plugin setup when you only need a small part of a kit, when the app does not use Plate UI components, or when a feature needs a different dependency boundary. ## Registry Names Kits are registry items. These names map to files in `components/editor/plugins` or `components/editor`. | Registry item | Installs | | --- | --- | | `basic-nodes-kit` | `BasicNodesKit`, `BasicBlocksKit`, and `BasicMarksKit` dependencies. | | `table-kit` | `TableKit` and table UI components. | | `media-kit` | `MediaKit` without a media upload API route. | | `media-uploadthing-kit` | `media-kit` plus the UploadThing API route. | | `editor-kit` | The full editable editor kit. | | `editor-base-kit` | The full static/base kit. | ## Next Steps | Task | Guide | | --- | --- | | Install registry items. | [Plate UI](/docs/installation/plate-ui) | | Configure editor creation. | [Editor Configuration](/docs/editor) | | Compose plugin APIs and options. | [Plugin Methods](/docs/plugin-methods) | | Render without an editable React editor. | [Static Rendering](/docs/static) | # Form Source: https://platejs.org/docs/form ## Registry URLs - Components index: https://platejs.org/r/registry.json - Docs index: https://platejs.org/r/registry-docs.json - Component content: https://platejs.org/r/{name} Any `` or `` in this page can be resolved at `https://platejs.org/r/{name}`. --- --- title: Form description: How to integrate Plate editor with react-hook-form. --- While Plate is typically used as an **uncontrolled** input, there are valid scenarios where you want to integrate the editor within a form library like [**react-hook-form**](https://www.react-hook-form.com) or the [**Form**](https://ui.shadcn.com/docs/components/form) component from **shadcn/ui**. This guide walks through best practices and common pitfalls. ## When to Integrate Plate with a Form - **Form Submission**: You want the editor's content to be included along with other fields (e.g., ``, `