Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
  • Feature Kits
  • Plugin
    • Plugin Methods
    • Plugin Shortcuts
    • Plugin Context
    • Plugin Components
    • Plugin Rules
    • Editing Behavior
    • Plugin Input Rules
  • Editor
    • Editor Methods
    • Controlled Value
  • Authored Changes
  • Performance
  • Static Rendering
  • HTML
  • Markdown
  • Form
  • TypeScript
  • Debugging
  • Unit Testing
  • Browser
  • Troubleshooting
  • Locations
  • Transactions
  • Serializing
  • Roots
  • Document Meta
  • Clipboard and Paste
  • Decorations, annotations, and widgets
  • Schema
  • History
  • Pagination
  • Annotations
  • DOM Coverage
  • External Text Views
  • Virtualized Rendering

Plugin Methods

PreviousNext

Author and configure Plate plugins.

Plugin authors put independent fields in the plugin constructor. Apps and registries finish a descriptor with one terminal .configure() call, including component when it renders an ordinary node.

Method map

MethodUse it forType behavior
.extend()Adapt an imported/prebuilt descriptor, use a constructor-inaccessible shared factory, or consume an earlier-stage type.Widens the plugin type.
.configure()Override existing fields for one consumer.
PluginPlugin Shortcuts

On This Page

Method mapUse constructor contextCommandsPure command buildersDeclare product codecsBind a componentConfigure existing fieldsConfigure related pluginsAdd React bindings
Build your editor
Production-ready AI template and reusable components.
Get all-access
Terminal and non-widening.

definePlugin() and definePlugin() own every independent author contribution:

FieldOwns
initialStateThe seed for this plugin's editor-local store.
apiFactory for immutable plugin-scoped services.
readSnapshot or transaction-local reads.
selectorsPure state-first derivations, including React subscriptions.
updateTransaction-bound plugin mutations.
Editor fieldsRead middleware, commands, corrections, state/effect/facet/selection descriptors, contributions, lifecycle, activation, and validation.
codecsPlugin-owned format decoding and encoding.

Callbacks receive the inferred Plugin Context, including editor, plugin, api, read, defineCodecs, store, and the active tx inside update contributions.

Use constructor context

Put independent fields and their context callbacks in the constructor. Use .extend() for an imported/prebuilt declaration or types introduced by an earlier stage.

counter-plugin.tsx
import { definePlugin, usePluginStore } from 'platejs/react';
 
export type CounterPluginState = {
  value: number;
};
 
export const CounterPlugin = definePlugin('counter', {
  api: ({ store }) => ({
    isEmpty: () => store.get('value') === 0,
  }),
  initialState: {
    value: 0,
  } satisfies CounterPluginState,
  selectors: {
    doubled: (state, factor: number) => state.value * factor,
    isEven: (state) => state.value % 2 === 0,
  },
  update: ({ store, tx }) => ({
    insertLabel: () => {
      tx.text.insert(`Count: ${store.get('value')}`);
    },
  }),
});
 
export function CounterValue() {
  const doubled = usePluginStore(CounterPlugin, 'doubled', 2);
  const isEven = usePluginStore(CounterPlugin, 'isEven');
 
  return (
    <span>
      {doubled} / {isEven ? 'even' : 'odd'}
    </span>
  );
}
counter-plugin.tsx
import { definePlugin, usePluginStore } from 'platejs/react';
 
export type CounterPluginState = {
  value: number;
};
 
export const CounterPlugin = definePlugin('counter', {
  api: ({ store }) => ({
    isEmpty: () => store.get('value') === 0,
  }),
  initialState: {
    value: 0,
  } satisfies CounterPluginState,
  selectors: {
    doubled: (state, factor:


















Let the constructor infer contributed API, read, selector, and update groups. Keep an exported state type only when package consumers need that contract.

After resolution, concrete editors expose the plugin API under its readable name. Generic package code uses the typed portal.

editor.api.counter.isEmpty();
editor.plugin(CounterPlugin).api.isEmpty();
editor.plugin(CounterPlugin).update.insertLabel();
editor.api.counter.isEmpty();
editor.plugin(CounterPlugin).api.isEmpty();
editor.plugin(CounterPlugin).update.insertLabel();

Inside a later update contribution, reuse an earlier update through the active transaction selector. Pass the capability name when the caller should not take a descriptor dependency:

export const CounterPairPlugin = CounterPlugin.extend(() => ({
  update: ({ tx }) => ({
    insertPair: () => {
      tx.plugin(CounterPlugin.name).insertLabel();
      tx.plugin(CounterPlugin.name).insertLabel();
    },
  }),
}));
export const CounterPairPlugin = CounterPlugin.extend(() => ({
  update: ({ tx }) => ({
    insertPair: () => {
      tx.plugin(CounterPlugin.name).insertLabel();
      tx.plugin(CounterPlugin.name).insertLabel();
    },
  }),
}));

The selector reuses the active transaction. Calling an editor portal one-shot update there would open a nested transaction.

Commands

Declare editor behavior directly on the plugin: schema, commands, corrections, read middleware, state/effect/facet/selection descriptors, contributions, lifecycle, activation, and validation.

single-line-plugin.ts
import { definePlugin, editorCommands } from 'platejs';
 
export const SingleLinePlugin = definePlugin('singleLine', {
  commands: ({ handle }) => [
    handle(editorCommands.insertBreak, ({ state }) =>
      state.transaction(() => {})
    ),
  ],
});
single-line-plugin.ts
import { definePlugin, editorCommands } from 'platejs';
 
export const SingleLinePlugin = definePlugin('singleLine', {
  commands: ({ handle }) => [
    handle(editorCommands.insertBreak, ({ state }) =>
      state.transaction(() => {})
    ),
  ],
});

Command and correction callbacks receive their declared capability context. When one needs an earlier plugin-owned store, read, API, or update group, add one staged .extend() and close over that capability. Do not pass owner context through a new helper parameter:

trigger-plugin.ts
import { definePlugin, editorCommands } from 'platejs';
 
export const TriggerPlugin = definePlugin('trigger', {
  initialState: { enabled: true },
}).extend(({ store }) => ({
  commands: ({ handle }) => [
    handle(editorCommands.insertBreak, ({ state }) => {
      if (!store.get('enabled')) return false;
 
      return state.transaction(() => {});
    }),
  ],
}));
trigger-plugin.ts
import { definePlugin, editorCommands } from 'platejs';
 
export const TriggerPlugin = definePlugin('trigger', {
  initialState: { enabled: true },
}).extend(({ store }) => ({
  commands: ({ handle }) => [
    handle(editorCommands.insertBreak, ({ state }) => {
      if (!store.get('enabled')) return false;
 
      return state.transaction(() => {});
    }),
  ],
}));

For an independently reusable Plate descriptor, import definePlugin from platejs and list the descriptor in dependencies.

Plugin-specific and editor-wide host services share the root api channel; Plate projects that API under the plugin name. Do not publish the same implementation twice.

Pure command builders

Use defineCommand from platejs when an action needs a typed, reusable intent that handlers can intercept. Its builder returns false or a frozen TransactionSpec. state.transaction(...) constructs that spec without publishing; editor.update.command(descriptor, input) applies the handled spec in one update.

Both state.transaction(...) and state.transaction.extend(...) callbacks must finish synchronously. A returned Promise or thenable throws and discards the draft. Complete asynchronous work before building the command.

Use handle(descriptor, handler) for fallback policy. Returning false runs the next handler or descriptor default. Use around(descriptor, handler) when the policy must rewrite downstream input or compose a prefix. An around handler delegates only when it calls next() or next.after(prefix); returning false rejects the command and stops fallback. next.after(prefix) evaluates downstream behavior against the state produced by that prefix. Plugin order determines handler order; the descriptor preserves input inference.

Declare product codecs

Build the MIME-keyed map with the callback's defineCodecs. This is the one inline inference anchor for codec callbacks.

records-plugin.ts
import { ContentSlice } from 'platejs';
import { definePlugin } from 'platejs';
 
export const RecordsPlugin = definePlugin('records', {
  codecs: ({ defineCodecs }) =>
    defineCodecs({
      'application/json': {
        scope: 'document',
        decode: ({ data }) => ContentSlice.fromJSON(JSON.parse(data)),
        encode: ({ slice }) => JSON.stringify(slice),
      },
    }),
});
records-plugin.ts
import { ContentSlice } from 'platejs';
import { definePlugin } from 'platejs';
 
export const RecordsPlugin = definePlugin('records', {
  codecs: ({ defineCodecs }) =>
    defineCodecs({
      'application/json': {
        scope: 'document',
        decode: ({ data }) => ContentSlice.fromJSON(JSON.parse(data)),
        encode: ({ slice }) => JSON.stringify(slice),
      },
    }),
});

Use document scope only when the format represents the complete document. Use defineCodecs(map) for self and product codecs. Use defineCodecs(TargetPlugin, map) for a foreign codec; the helper injects the target into every HTML rule. The map's 'text/html' value accepts one schema-aware rule or a non-empty ordered rule tuple when the plugin owns multiple HTML representations. Keep that tuple in the same map. Do not author a direct codec map or add target manually.

Bind a component

Bind an ordinary node component in the same terminal .configure() call as the descriptor's other consumer overrides.

plugins.tsx
import { ParagraphPlugin } from 'platejs/react';
 
import { ParagraphElement } from '@/components/editor/paragraph';
 
export const AppParagraphPlugin = ParagraphPlugin.configure({
  component: ParagraphElement,
  shortcuts: {
    toggle: { keys: 'mod+alt+0' },
  },
});
plugins.tsx
import { ParagraphPlugin } from 'platejs/react';
 
import { ParagraphElement } from '@/components/editor/paragraph';
 
export const AppParagraphPlugin = ParagraphPlugin.configure({
  component: ParagraphElement,
  shortcuts: {
    toggle: { keys: 'mod+alt+0' },
  },
});

Component binding preserves the plugin type. Do not assign the node component through a renderer registry field.

Configure existing fields

Use .configure() once, where an app or registry installs a plugin, to change fields already declared by the author.

plugins.tsx
import { LinkPlugin } from 'platejs/react';
 
export const AppLinkPlugin = LinkPlugin.configure({
  initialState: {
    allowedSchemes: ['http', 'https', 'mailto'],
  },
});
plugins.tsx
import { LinkPlugin } from 'platejs/react';
 
export const AppLinkPlugin = LinkPlugin.configure({
  initialState: {
    allowedSchemes: ['http', 'https', 'mailto'],
  },
});

Object configs use Plate's merge rules: objects merge deeply, arrays replace, and initialState shallow-merges. .configure() cannot publish a new capability and must be the final call.

Configure related plugins

Configure a required dependency on its own descriptor and place that complete descriptor beside the owner in the app or registry plugin array.

type CellPluginState = {
  padding: number;
};
 
const CellPlugin = definePlugin('cell', {
  initialState: {
    padding: 12,
  } satisfies CellPluginState,
});
 
const GridPlugin = definePlugin('grid', {
  dependencies: [CellPlugin],
});
 
export const AppGridPlugins = [
  GridPlugin,
  CellPlugin.configure({ initialState: { padding: 8 } }),
];
type CellPluginState = {
  padding: number;
};
 
const CellPlugin = definePlugin('cell', {
  initialState: {
    padding: 12,
  } satisfies CellPluginState,
});
 
const GridPlugin = definePlugin('grid', {
  dependencies: [CellPlugin],
});
 
export const AppGridPlugins = [
  GridPlugin,
  CellPlugin.configure({ initialState: { padding: 8 } }),
];

Optional capabilities are ordinary array entries. Terminal configurations derived from the same authored plugin compose in array order: earlier fields survive unless a later configuration defines the same field. Unrelated plugins and divergent authoring branches cannot share a name.

Add React bindings

Use toReactPlugin() in the owning React entrypoint to add React behavior to a reusable headless plugin. App consumers do not insert a conversion merely to set a component.

script-plugin.tsx
import { BaseScriptPlugin } from 'platejs';
import { EditorLeaf, toReactPlugin } from 'platejs/react';
 
export const ScriptPlugin = toReactPlugin(BaseScriptPlugin, {
  component: (props) => (
    <EditorLeaf
      {...props}
      as={props.leaf.script === 'sub' ? 'sub' : 'sup'}
    >
      {props.children}
    </EditorLeaf>
  ),
});
script-plugin.tsx
import { BaseScriptPlugin } from 'platejs';
import { EditorLeaf, toReactPlugin } from 'platejs/react';
 
export const ScriptPlugin = toReactPlugin(BaseScriptPlugin, {
  component: (props) => (
    <EditorLeaf
      {...props}
      as={props.leaf.script === 'sub' ? 'sub' : 'sup'}
    >
      {props.children}
    </EditorLeaf>
  ),
});

The package's React owner publishes ScriptPlugin; static/RSC code can bind a server-safe component directly on BaseScriptPlugin without importing platejs/react.

number
)
=>
state.value
*
factor,
isEven: (state) => state.value % 2 === 0,
},
update: ({ store, tx }) => ({
insertLabel: () => {
tx.text.insert(`Count: ${store.get('value')}`);
},
}),
});
export function CounterValue() {
const doubled = usePluginStore(CounterPlugin, 'doubled', 2);
const isEven = usePluginStore(CounterPlugin, 'isEven');
return (
<span>
{doubled} / {isEven ? 'even' : 'odd'}
</span>
);
}