From Slate to Plate

PreviousNext

Move a Slate React editor to Plate's editor, plugin, and rendering model.

Plate keeps Slate's document model and moves editor setup, rendering, handlers, and command wiring into plugins. Migrate the editor shell first, then move custom rendering and behavior into plugins.

Install

pnpm add platejs
pnpm add platejs

Use feature packages only for the nodes, marks, or behavior you add to the editor. Plate UI users should start with Plate UI instead of rebuilding every component by hand.

Migration Map

Slate surfacePlate surface
createEditor() plus withReact()usePlateEditor() in React components, or createPlateEditor() in factories and tests.
<Slate> plus <Editable><Plate> plus <PlateContent>.
renderElement / renderLeaf switch statementsPlugin components through .withComponent() or node.component.
withX(editor) plugin functions.overrideEditor() for wrappers, .extend*() for new APIs and transforms.
Top-level event handlers on EditablePlugin handlers or shortcuts.
Transforms.* importseditor.tf.* transforms.
Editor.* importseditor.api.* queries.

Editor Shell

Move the editor value into the editor creation call and render the editable with PlateContent.

components/editor.tsx
'use client';
 
import { Plate, PlateContent, usePlateEditor } from 'platejs/react';
 
const initialValue = [
  {
    children: [{ text: 'Hello Plate.' }],
    type: 'p',
  },
];
 
export function Editor() {
  const editor = usePlateEditor({
    value: initialValue,
  });
 
  return (
    <Plate editor={editor}>
      <PlateContent className="p-4" />
    </Plate>
  );
}
components/editor.tsx
'use client';
 
import { Plate, PlateContent, usePlateEditor } from 'platejs/react';
 
const initialValue = [
  {
    children: [{ text: 'Hello Plate.' }],
    type: 'p',
  },
];
 
export function Editor() {
  const editor = usePlateEditor({
    value: initialValue,
  });
 
  return (
    <Plate editor={editor}>
      <PlateContent className="p-4" />
    </Plate>
  );
}

Use createPlateEditor() when the editor is created outside React memoization.

lib/create-editor.ts
import { createPlateEditor } from 'platejs/react';
 
export const editor = createPlateEditor({
  value: [
    {
      children: [{ text: 'Draft' }],
      type: 'p',
    },
  ],
});
lib/create-editor.ts
import { createPlateEditor } from 'platejs/react';
 
export const editor = createPlateEditor({
  value: [
    {
      children: [{ text: 'Draft' }],
      type: 'p',
    },
  ],
});

Custom Elements

Replace renderElement branches with node plugins. Use .withComponent() when the only change is the React component.

components/editor/paragraph-plugin.tsx
import {
  ParagraphPlugin,
  PlateElement,
  type PlateElementProps,
} from 'platejs/react';
 
export function ParagraphElement({
  children,
  ...props
}: PlateElementProps) {
  return (
    <PlateElement className="m-0 px-0 py-1" {...props}>
      {children}
    </PlateElement>
  );
}
 
export const AppParagraphPlugin = ParagraphPlugin.withComponent(
  ParagraphElement
);
components/editor/paragraph-plugin.tsx
import {
  ParagraphPlugin,
  PlateElement,
  type PlateElementProps,
} from 'platejs/react';
 
export function ParagraphElement({
  children,
  ...props
}: PlateElementProps) {
  return (
    <PlateElement className="m-0 px-0 py-1" {...props}>
      {children}
    </PlateElement>
  );
}
 
export const AppParagraphPlugin = ParagraphPlugin.withComponent(
  ParagraphElement
);

If your Slate document stores a custom type like paragraph, keep that type on the plugin.

components/editor/paragraph-plugin.tsx
export const AppParagraphPlugin = ParagraphPlugin.configure({
  node: { type: 'paragraph' },
}).withComponent(ParagraphElement);
components/editor/paragraph-plugin.tsx
export const AppParagraphPlugin = ParagraphPlugin.configure({
  node: { type: 'paragraph' },
}).withComponent(ParagraphElement);

Custom Behavior

Use .overrideEditor() when the Slate plugin wrapped an existing editor method.

components/editor/limit-exclamation-plugin.tsx
import { createPlatePlugin } from 'platejs/react';
 
export const LimitExclamationPlugin = createPlatePlugin({
  key: 'limitExclamation',
}).overrideEditor(({ tf: { insertText } }) => ({
  transforms: {
    insertText(text, options) {
      insertText(text === '!' ? '.' : text, options);
    },
  },
}));
components/editor/limit-exclamation-plugin.tsx
import { createPlatePlugin } from 'platejs/react';
 
export const LimitExclamationPlugin = createPlatePlugin({
  key: 'limitExclamation',
}).overrideEditor(({ tf: { insertText } }) => ({
  transforms: {
    insertText(text, options) {
      insertText(text === '!' ? '.' : text, options);
    },
  },
}));

Use .extendEditorApi() or .extendEditorTransforms() when the plugin adds a new method.

components/editor/signature-plugin.tsx
import { createPlatePlugin } from 'platejs/react';
 
export const SignaturePlugin = createPlatePlugin({
  key: 'signature',
}).extendEditorTransforms(({ editor }) => ({
  insertSignature() {
    editor.tf.insertText(' - Plate');
  },
}));
components/editor/signature-plugin.tsx
import { createPlatePlugin } from 'platejs/react';
 
export const SignaturePlugin = createPlatePlugin({
  key: 'signature',
}).extendEditorTransforms(({ editor }) => ({
  insertSignature() {
    editor.tf.insertText(' - Plate');
  },
}));

Handlers And Shortcuts

Move editor events into the plugin that owns the behavior.

components/editor/tab-plugin.tsx
import { createPlatePlugin } from 'platejs/react';
 
export const TabPlugin = createPlatePlugin({
  key: 'tab',
  handlers: {
    onKeyDown: ({ event }) => {
      if (event.key !== 'Tab') return false;
 
      event.preventDefault();
 
      return true;
    },
  },
});
components/editor/tab-plugin.tsx
import { createPlatePlugin } from 'platejs/react';
 
export const TabPlugin = createPlatePlugin({
  key: 'tab',
  handlers: {
    onKeyDown: ({ event }) => {
      if (event.key !== 'Tab') return false;
 
      event.preventDefault();
 
      return true;
    },
  },
});

Use shortcuts when the key combination should call a plugin API, transform, or explicit handler.

components/editor/save-plugin.tsx
import { createPlatePlugin } from 'platejs/react';
 
export const SavePlugin = createPlatePlugin({
  key: 'save',
}).extend({
  shortcuts: {
    draft: {
      keys: 'mod+s',
      handler: ({ event }) => {
        event.preventDefault();
 
        return true;
      },
    },
  },
});
components/editor/save-plugin.tsx
import { createPlatePlugin } from 'platejs/react';
 
export const SavePlugin = createPlatePlugin({
  key: 'save',
}).extend({
  shortcuts: {
    draft: {
      keys: 'mod+s',
      handler: ({ event }) => {
        event.preventDefault();
 
        return true;
      },
    },
  },
});

API Calls

Plate keeps Slate-style direct methods for compatibility, but plugin code should use the namespaced API and transform surfaces.

editor-commands.ts
editor.tf.toggleMark('bold');
editor.tf.insertText('Hello');
 
const text = editor.api.string([]);
 
if (editor.selection) {
  const isStart = editor.api.isStart(editor.selection.anchor, []);
}
editor-commands.ts
editor.tf.toggleMark('bold');
editor.tf.insertText('Hello');
 
const text = editor.api.string([]);
 
if (editor.selection) {
  const isStart = editor.api.isStart(editor.selection.anchor, []);
}

Headless Code

Use createSlateEditor from platejs for non-React importers, serializers, transforms, and tests.

lib/headless-editor.ts
import { createSlateEditor } from 'platejs';
 
export const editor = createSlateEditor({
  value: [
    {
      children: [{ text: 'Headless document.' }],
      type: 'p',
    },
  ],
});
lib/headless-editor.ts
import { createSlateEditor } from 'platejs';
 
export const editor = createSlateEditor({
  value: [
    {
      children: [{ text: 'Headless document.' }],
      type: 'p',
    },
  ],
});