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 Context

PreviousNext

Use editor, plugin, store, API, and transaction context inside Plate plugins.

Plugin context is the object Plate passes to plugin configuration callbacks, events, native capabilities, transaction commands, and render components. It gives you the resolved editor, current plugin name and schema handles, api, and plugin store without reaching through global state. Use it inside plugin-owned code; use editor methods or React hooks when code runs outside a plugin callback.

Context Shape

PlatePluginContext extends the shared plugin context with a React PlateEditor. The same helper names are available in headless Plate base plugins, but the editor type is BaseEditor.

PropertyUse for
editorThe resolved editor instance.
Plugin ShortcutsPlugin Components

On This Page

Context ShapePlugin MethodsExtension CallbacksProperty MutationsNative Capability InferenceCodec InferenceAnother PluginReact ComponentsStore StateAPI Reference
Build your editor
Production-ready AI template and reusable components.
Get all-access
plugin
The resolved plugin configuration for the current plugin.
nameCapability identity and API/update namespace. Never use it as an element type or property key.
schema.typePersisted identity for an element-owning plugin.
schema.keyPersisted identity for a primary-property plugin.
schema.propertiesCompiled handles for additional properties declared by the current plugin.
apiAPI owned by the current plugin.
readState-bound reads owned by the current plugin.
updateOne-shot updates owned by the current plugin.
storeRead, update, or subscribe to the current plugin's editor-local state.
defineCodecs(map)Bind a self/product codec map to this plugin's inferred schema.
defineCodecs(TargetPlugin, map)Bind a foreign codec map to an exact descriptor and inject its target.

Plugin Methods

Event callbacks receive context plus the event or lifecycle payload. Use the context helpers instead of closing over editor state.

counter-plugin.ts
import { definePlatePlugin } from 'platejs/react';
 
export type CounterPluginState = {
  count: number;
  enabled: boolean;
};
 
export const CounterPlugin = definePlatePlugin('counter', {
  initialState: {
    count: 0,
    enabled: true,
  } satisfies CounterPluginState,
  on: {
    keyDown: ({ event, name, store }) => {
      if (!store.get('enabled')) return;
 
      if (event.key === '+') {
        store.set((state) => {
          state.count += 1;
        });
        console.info(`${name} count incremented`);
      }
    },
  },
});
counter-plugin.ts
import { definePlatePlugin } from 'platejs/react';
 
export type CounterPluginState = {
  count: number;
  enabled: boolean;
};
 
export const CounterPlugin = definePlatePlugin('counter', {
  initialState: {
    count: 0,
    enabled: true,
  } satisfies CounterPluginState,
  on: {
    keyDown: ({ event, name, store }) => {
      if (!store.get('enabled')) 









store is scoped to CounterPlugin in this example.

Extension Callbacks

Configuration, native capability, selector, API, transaction, and editor override callbacks also receive plugin context.

counter-plugin.ts
import { definePlatePlugin } from 'platejs/react';
 
export type CounterPluginState = {
  count: number;
};
 
export const CounterPlugin = definePlatePlugin('counter', {
  initialState: {
    count: 0,
  } satisfies CounterPluginState,
  api: ({ store }) => ({
    isEmpty: () => store.get('count') === 0,
  }),
  selectors: {
    label: (state) => `Count: ${state.count}`,
  },
});
counter-plugin.ts
import { definePlatePlugin } from 'platejs/react';
 
export type CounterPluginState = {
  count: number;
};
 
export const CounterPlugin = definePlatePlugin('counter', {
  initialState: {
    count: 0,
  } satisfies CounterPluginState,
  api: ({ store }) => ({
    isEmpty: () => store.get('count') === 0,
  }),
  selectors: {
    label: (state) => `Count: ${state

Selectors are readable through store.get and subscribable through usePluginStore. They are pure state-first functions and cannot read the editor or another plugin.

Property Mutations

Inside update, pass a property patch object. Plate infers each key and value from the current plugin and its required dependencies:

const LineHeightPlugin = defineBasePlugin('lineHeight', {
  schema: {
    properties: {
      lineHeight: schema.elementProperty(property.number()),
    },
  },
  update: ({ tx }) => ({
    set: (value: number) => tx.nodes.set({ lineHeight: value }),
    unset: () => tx.nodes.unset('lineHeight'),
  }),
});
const LineHeightPlugin = defineBasePlugin('lineHeight', {
  schema: {
    properties: {
      lineHeight: schema.elementProperty(property.number()),
    },
  },
  update: ({ tx }) => ({
    set: (value: number) => tx.nodes.set({ lineHeight: value }),
    unset: () => tx.nodes.unset('lineHeight'),
  }),
});

Use the property handle's exact key as a computed object key for an aliased property. Prefix families and cross-node behavior belong in a semantic plugin update method.

Native Capability Inference

Declare native Plite fields directly in the constructor or a staged .extend() callback. Their capability-specific helpers and Plate plugin context are inferred together. Extract domain inputs instead of the whole Plate plugin context.

Use defineExtension from @platejs/plite only for independently reusable standalone Plite descriptors. See Plugin Methods.

Codec Inference

Use defineCodecs in the constructor's codecs callback. It is the codec map's single inference anchor:

strong-plugin.ts
import { defineBasePlugin, property } from 'platejs';
 
export const StrongPlugin = defineBasePlugin('strong', {
  codecs: ({ defineCodecs }) =>
    defineCodecs({
      'text/html': {
        decode: () => true,
        decodeOnly: true,
        match: [{ tag: 'strong' }],
      },
    }),
  schema: { mark: property.boolean() },
});
strong-plugin.ts
import { defineBasePlugin, property } from 'platejs';
 
export const StrongPlugin = defineBasePlugin('strong', {
  codecs: ({ defineCodecs }) =>
    defineCodecs({
      'text/html': {
        decode: () => true,
        decodeOnly: true,
        match: [{ tag: 'strong' }],
      },
    }),
  schema: { mark: property.boolean() },
});

The one-argument form owns self and product maps. For a foreign contribution, call defineCodecs(TargetPlugin, map); Plate injects the target into every HTML rule. Keep the map MIME-keyed, and use either one 'text/html' rule or a non-empty ordered rule tuple. Do not author direct codec maps or annotate the callbacks.

Another Plugin

Use editor.plugin(Plugin) when plugin-owned code needs another plugin's consumer portal.

link-aware-plugin.ts
import { LinkPlugin } from '@platejs/link/react';
import { definePlatePlugin } from 'platejs/react';
 
export const LinkAwarePlugin = definePlatePlugin('linkAware', {
  dependencies: [LinkPlugin],
  on: {
    keyDown: ({ editor, event }) => {
      if (event.key !== 'Enter') return;
 
      const link = editor.plugin(LinkPlugin);
 
      console.info(`Link element type: ${link.schema.type}`);
    },
  },
});
link-aware-plugin.ts
import { LinkPlugin } from '@platejs/link/react';
import { definePlatePlugin } from 'platejs/react';
 
export const LinkAwarePlugin = definePlatePlugin('linkAware', {
  dependencies: [LinkPlugin],
  on: {
    keyDown: ({ editor, event }) => {
      if (event.key !== 'Enter') return;
 
      const link = editor.plugin(LinkPlugin);
 
      console.info(`Link element type: ${link.schema.type}`);
    },
  },
});

Declare a dependency when the plugin cannot work without that capability. Plate installs dependencies before their dependents, so the portal is available in the handler. Keep cross-plugin writes rare; they couple two plugins tightly.

React Components

Use useEditorPlugin inside a component rendered under <Plate>. It returns the plugin's flat consumer portal. Call useEditor() separately when the component also needs the editor.

counter-badge.tsx
import { useEditorPlugin, usePluginStore } from 'platejs/react';
 
import { CounterPlugin } from './counter-plugin';
 
export function CounterBadge() {
  const { name } = useEditorPlugin(CounterPlugin);
  const count = usePluginStore(CounterPlugin, 'count');
  const label = usePluginStore(CounterPlugin, 'label');
 
  return (
    <span data-plugin-name={name}>
      {label} ({count})
    </span>
  );
}
counter-badge.tsx
import { useEditorPlugin, usePluginStore } from 'platejs/react';
 
import { CounterPlugin } from './counter-plugin';
 
export function CounterBadge() {
  const { name } = useEditorPlugin(CounterPlugin);
  const count = usePluginStore(CounterPlugin, 'count');
  const label = usePluginStore(CounterPlugin, 'label');
 
  return (
    <span data-plugin-name={name}>
      {label} ({count})
    </span>
  );
}

Use a selector callback when a component needs a derived value from several state fields.

counter-badge.tsx
import { usePluginStore } from 'platejs/react';
 
import { CounterPlugin } from './counter-plugin';
 
export function CounterStatus() {
  const status = usePluginStore(CounterPlugin, (state) =>
    state.count === 0 ? 'empty' : 'active'
  );
 
  return <span>{status}</span>;
}
counter-badge.tsx
import { usePluginStore } from 'platejs/react';
 
import { CounterPlugin } from './counter-plugin';
 
export function CounterStatus() {
  const status = usePluginStore(CounterPlugin, (state) =>
    state.count === 0 ? 'empty' : 'active'
  );
 
  return <span>{status}</span>;
}

For code outside the nearest <Plate> provider, pass an editor explicitly with useEditorPluginStore.

Store State

Plugin state is stored per editor. Updating one editor's plugin store does not update another editor.

counter-plugin.ts
export const CounterPluginWithInitialCount = CounterPlugin.extend(
  ({ store }) => ({
    initialState: {
      count: store.get().count + 1,
    },
  })
);
counter-plugin.ts
export const CounterPluginWithInitialCount = CounterPlugin.extend(
  ({ store }) => ({
    initialState: {
      count: store.get().count + 1,
    },
  })
);

store.set accepts either a partial object or a draft callback.

counter-actions.ts
import type { PlateEditor } from 'platejs/react';
 
import { CounterPlugin } from './counter-plugin';
 
export function resetCounter(editor: PlateEditor) {
  const { store } = editor.plugin(CounterPlugin);
 
  store.set({
    count: 1,
  });
 
  store.set((draft) => {
    draft.count += 1;
  });
}
counter-actions.ts
import type { PlateEditor } from 'platejs/react';
 
import { CounterPlugin } from './counter-plugin';
 
export function resetCounter(editor: PlateEditor) {
  const { store } = editor.plugin(CounterPlugin);
 
  store.set({
    count: 1,
  });
 
  store.set((draft) => {
    draft.count += 1;
  });
}

Plate throws when store.get or usePluginStore targets a missing state field or selector.

Always pass the plugin descriptor to usePluginStore and useEditorPluginStore. A name-only object has no state contract for TypeScript to infer.

API Reference

HelperScopeNotes
editor.plugin(plugin)Any editor code.Opens the installed plugin's typed consumer portal.
useEditorPlugin(plugin, id?)React under <Plate>.Returns the installed plugin's flat consumer portal.
usePluginStore(plugin, key, ...args)React under <Plate>.Subscribes to one state field or named selector.
usePluginStore(plugin, selector, options?)React under <Plate>.Subscribes to a value derived from plugin state.
useEditorPluginStore(editor, plugin, key, ...args)React with explicit editor.Use outside the closest editor provider.
useEditorPluginStore(editor, plugin, selector, options?)React with explicit editor.Explicit-editor variant of usePluginStore.

For plugin extension methods, see Plugin Methods. For plugin configuration, see Plugin.

return
;
if (event.key === '+') {
store.set((state) => {
state.count += 1;
});
console.info(`${name} count incremented`);
}
},
},
});
.
count
}`
,
},
});