From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
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
  • Performance
  • Static Rendering
  • HTML
  • Markdown
  • Form
  • TypeScript
  • Debugging
  • Unit Testing
  • Browser
  • Troubleshooting

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.Terminal and non-widening.
PluginPlugin Shortcuts

On This Page

Method MapUse Constructor ContextInstall Plite CapabilitiesDeclare Product CodecsBind A ComponentConfigure Existing FieldsConfigure Related PluginsConvert Plite Plugins
Build your editor
Production-ready AI template and reusable components.
Get all-access

defineBasePlugin() and definePlatePlugin() 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.
Native Plite 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 { definePlatePlugin, usePluginStore } from 'platejs/react';
 
export type CounterPluginState = {
  value: number;
};
 
export const CounterPlugin = definePlatePlugin('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 { definePlatePlugin, usePluginStore } from 'platejs/react';
 
export type CounterPluginState = {
  value: number;
};
 
export const CounterPlugin = definePlatePlugin('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 group:

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

Calling a portal one-shot update there would open a nested transaction.

Install Plite Capabilities

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

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

Native callbacks receive only their Plite 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 { defineBasePlugin, editorCommands } from 'platejs';
 
export const TriggerPlugin = defineBasePlugin('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 { defineBasePlugin, editorCommands } from 'platejs';
 
export const TriggerPlugin = defineBasePlugin('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 Plite descriptor, import defineExtension from @platejs/plite and list the descriptor in dependencies.

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

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/plite';
import { defineBasePlugin } from 'platejs';
 
export const RecordsPlugin = defineBasePlugin('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/plite';
import { defineBasePlugin } from 'platejs';
 
export const RecordsPlugin = defineBasePlugin('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/link/react';
 
export const AppLinkPlugin = LinkPlugin.configure({
  initialState: {
    allowedSchemes: ['http', 'https', 'mailto'],
  },
});
plugins.tsx
import { LinkPlugin } from '@platejs/link/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 = definePlatePlugin('cell', {
  initialState: {
    padding: 12,
  } satisfies CellPluginState,
});
 
const GridPlugin = definePlatePlugin('grid', {
  dependencies: [CellPlugin],
});
 
export const AppGridPlugins = [
  GridPlugin,
  CellPlugin.configure({ initialState: { padding: 8 } }),
];
type CellPluginState = {
  padding: number;
};
 
const CellPlugin = definePlatePlugin('cell', {
  initialState: {
    padding: 12,
  } satisfies CellPluginState,
});
 
const GridPlugin = definePlatePlugin('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.

Convert Plite Plugins

Use toPlatePlugin() in the owning React entrypoint to publish a reusable Plate descriptor or add Plate-only behavior. App consumers do not insert a conversion merely to set a component.

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

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>
);
}