Plate plugins own one capability: schema, behavior, state, services, reads, updates, rendering, or a deliberate combination of them. Define independent fields at creation and use terminal configuration only for app-owned values.
The most basic plugin configuration requires only a name:
const MyPlugin = definePlatePlugin("minimal", {});const MyPlugin = definePlatePlugin("minimal", {});While this plugin doesn't do anything yet, it's a starting point for more complex configurations.
The .configure method allows you to configure an existing plugin:
const ConfiguredPlugin = MyPlugin.configure({
initialState: {
myOption: "new value",
},
});const ConfiguredPlugin = MyPlugin.configure({
initialState: {
myOption: "new value",
},
});Schema plugins declare persisted document identity and structure under
schema. React components stay on the plugin root.
Declare a new element with schema.element:
import { schema } from "platejs";
const NoticePlugin = definePlatePlugin("noticeFeature", {
schema: {
element: { ...schema.element.textBlock(), type: "notice" },
},
});import { schema } from "platejs";
const NoticePlugin = definePlatePlugin("noticeFeature", {
schema: {
element: { ...schema.element.textBlock(), type: "notice" },
},
});You can associate a component with your element. See Plugin Components for more information.
import { schema } from "platejs";
const NoticePlugin = definePlatePlugin("noticeFeature", {
component: NoticeElement,
schema: {
element: { ...schema.element.textBlock(), type: "notice" },
},
});import { schema } from "platejs";
const NoticePlugin = definePlatePlugin("noticeFeature", {
component: NoticeElement,
schema: {
element: { ...schema.element.textBlock(), type: "notice" },
},
});Plate treats each non-inline element as normal-flow block content unless its
schema declares blockContent: false. Use that flag for structural internals,
not for blocks that should remain selectable.
const RowPlugin = definePlatePlugin("row", {
schema: {
element: {
...schema.element.textBlock(),
blockContent: false,
},
},
});const RowPlugin = definePlatePlugin("row", {
schema: {
element: {
...schema.element.textBlock(),
blockContent: false,
},
},
});Read the compiled result through the Plate schema API:
editor.read.schema.isBlockContent(element);editor.read.schema.isBlockContent(element);This classification is independent of
editor.read.nodes.isSelectable(element). Content containers use
plugins.blockContent(...) when they declare which normal-flow blocks they
accept.
Element behavior lives inside schema.element. Declare a boolean text property
with a property descriptor under schema.mark:
import { property, schema } from "platejs";
const CustomLinkPlugin = definePlatePlugin("customLink", {
schema: {
element: {
content: schema.content.text({ default: "text", min: 1 }),
inline: true,
},
},
});
const CustomImagePlugin = definePlatePlugin("customImage", {
schema: { element: { void: "block" } },
});
const CustomBoldPlugin = definePlatePlugin("customBold", {
schema: {
mark: property.boolean({ default: false, omitDefault: true }),
},
});import { property, schema } from "platejs";
const CustomLinkPlugin = definePlatePlugin("customLink", {
schema: {
element: {
content: schema.content.text({ default: "text", min: 1 }),
inline: true,
},
},
});
const CustomImagePlugin = definePlatePlugin("customImage", {
schema: { element: { void: "block" } },
});
const CustomBoldPlugin = definePlatePlugin("customBold", {
schema: {
mark: property.boolean({ default: false
Rather than declare an element or text property, you may want to customize the behavior of your editor. Plugin fields describe that behavior.
The rules property allows you to configure common editing behaviors like breaking, deleting, and merging nodes without overriding editor methods. This is a powerful way to define intuitive interactions for your custom elements.
For example, you can define what happens when a user presses Enter in an empty heading, or Backspace at the start of a blockquote.
import { HeadingPlugin } from "@platejs/basic-nodes/react";
HeadingPlugin.configure({
rules: {
break: { empty: "reset" },
},
});import { HeadingPlugin } from "@platejs/basic-nodes/react";
HeadingPlugin.configure({
rules: {
break: { empty: "reset" },
},
});See the Plugin Rules guide for a complete list of available rules and actions.
The on field owns both editor lifecycle and React DOM events. A DOM handler receives a
PlatePluginContext & { event } object.
Child names do not repeat the on prefix. Lifecycle handlers use names such as
commit, nodeChange, and textChange; DOM handlers use keyDown, paste,
and click.
const ExamplePlugin = definePlatePlugin("example", {
on: {
commit: ({ editor, snapshot }) => {
console.info(editor, snapshot.children);
},
keyDown: ({ editor, event }) => {
console.info(`You pressed ${event.key}`);
},
},
});const ExamplePlugin = definePlatePlugin("example", {
on: {
commit: ({ editor, snapshot }) => {
console.info(editor, snapshot.children);
},
keyDown: ({ editor, event }) => {
console.info(`You pressed ${event.key}`);
},
},
});You may want to inject a class name or CSS property into any node having a certain property. For example, the following plugin sets the textAlign CSS property on paragraphs with a textAlign property.
import { PLUGINS, property, schema, target } from "platejs";
const TextAlignPlugin = definePlatePlugin("textAlign", {
codecs: ({ defineCodecs }) =>
defineCodecs({
"text/html": {
decode: ({ element }) => element.style.textAlign || undefined,
encode: ({ value }) => ({ style: { textAlign: value } }),
match: [
{
style: {
textAlign: ["start", "left", "center", "right", "end", "justify"],
},
},
],
},
}),
inject: {
isBlock: true,
nodeProps: {
defaultNodeValue: "start",
styleKey: "textAlign",
validNodeValues: ["start", "left", "center", "right", "end", "justify"],
},
},
schema: ({ targetElementTypes }) => ({
properties: {
textAlign: schema.elementProperty(property.string(), {
target: target.types(targetElementTypes),
typeChange: "preserve-if-allowed",
}),
},
}),
targetPlugins: [PLUGINS.paragraph],
update: ({ tx }) => ({
set: (value: string) => tx.nodes.set({ textAlign: value }),
}),
});import { PLUGINS, property, schema, target } from "platejs";
const TextAlignPlugin = definePlatePlugin("textAlign", {
codecs: ({ defineCodecs }) =>
defineCodecs({
"text/html": {
decode: ({ element }) => element.style.textAlign || undefined,
encode: ({ value }) => ({ style: { textAlign: value } }),
match: [
{
style: {
textAlign: ["start", "left", "center", "right", "end", "justify"],
},
},
inject.nodeProps owns rendering, while the constructor's codecs callback
binds the bidirectional HTML claim to this plugin's schema. defineCodecs
keeps value inferred from the property declaration. Both paths use the
author callback's compiled schema.properties.textAlign handle. Consumers use
typed node fields or the plugin's semantic capabilities instead of reading that
property map. Its name identifies only the capability namespace.
.configure() and .extend() do not change schema identity. The schema
passed to createPlateEditor or usePlateEditor may remap an element type or
relationship; plugin-owned property keys remain fixed. A plugin update passes
one atomic property patch,
so every key and value comes from the plugin's shallow schema contract. An
alias uses the exact author handle key as a computed object key.
A paragraph node affected by the plugin looks like this:
const paragraph = {
type: 'paragraph',
textAlign: 'right',
children: [{ text: 'This paragraph is aligned to the right!' }],
};const paragraph = {
type: 'paragraph',
textAlign: 'right',
children: [{ text: 'This paragraph is aligned to the right!' }],
};Put state-bound queries under read, immutable services under api, and
document mutations under update. Constructor callbacks receive plugin
context.
export type CustomPluginState = {
prefix: string;
};
const CustomPlugin = definePlatePlugin("custom", {
api: ({ store }) => ({
getPrefix: () => store.get("prefix"),
}),
initialState: {
prefix: "Note: ",
} satisfies CustomPluginState,
update: ({ store, tx }) => ({
insertPrefix: () => {
tx.text.insert(store.get("prefix"));
},
}),
});export type CustomPluginState = {
prefix: string;
};
const CustomPlugin = definePlatePlugin("custom", {
api: ({ store }) => ({
getPrefix: () => store.get("prefix"),
}),
initialState: {
prefix: "Note: ",
} satisfies CustomPluginState,
update: ({ store, tx }) => ({
insertPrefix: () => {
tx.text.insert(store.get("prefix"
After the plugin resolves, concrete editors infer its services on the root API and one-shot writes use the name-scoped update helper:
editor.api.custom.getPrefix();
editor.update.custom.insertPrefix();editor.api.custom.getPrefix();
editor.update.custom.insertPrefix();Generic package code can reach the same immutable API object through
editor.plugin(CustomPlugin).api.getPrefix().
Plate plugins declare Plite capabilities directly at the plugin root. Use
readMiddleware, commands, corrections, stateFields, effectTypes,
facetProviders, contributions, on, activate, and
validate without a second wrapper.
import { defineStateField } from "platejs";
import { definePlatePlugin } from "platejs/react";
const enabledField = defineStateField({
initial: false,
key: "customState.enabled",
});
const CustomStatePlugin = definePlatePlugin("customState", {
stateFields: [enabledField],
read: ({ state }) => ({
enabled: () => state.getField(enabledField),
}),
on: {
commit: ({ commit }) => {
console.info(commit.changed);
},
},
});import { defineStateField } from "platejs";
import { definePlatePlugin } from "platejs/react";
const enabledField = defineStateField({
initial: false,
key: "customState.enabled",
});
const CustomStatePlugin = definePlatePlugin("customState", {
stateFields: [enabledField],
read: ({ state }) => ({
enabled: () => state.getField(enabledField),
}),
on: {
commit: ({ commit }) => {
console.info(commit.changed);
Put every independent capability in the constructor. Use .extend() only for
an imported/prebuilt descriptor, a shared factory the constructor cannot
access, or an earlier-stage type.
An independently reusable standalone Plite descriptor uses
defineExtension from @platejs/plite. See
Plugin Methods.
Each plugin has its own store, which can be used to manage plugin-specific state.
type MyPluginState = {
count: number;
};
const MyPlugin = definePlatePlugin("myPlugin", {
initialState: {
count: 0,
} satisfies MyPluginState,
on: {
click: ({ store }) => {
store.set({ count: 1 });
},
},
});type MyPluginState = {
count: number;
};
const MyPlugin = definePlatePlugin("myPlugin", {
initialState: {
count: 0,
} satisfies MyPluginState,
on: {
click: ({ store }) => {
store.set({ count: 1 });
},
},
});You can access and update the store using the following methods:
// Get the current value
const count = editor.plugin(MyPlugin).store.get("count");
// Set a new value
editor.plugin(MyPlugin).store.set({ count: 5 });
// Update the value based on the previous state
editor.plugin(MyPlugin).store.set((state) => {
state.count += 1;
});// Get the current value
const count = editor.plugin(MyPlugin).store.get("count");
// Set a new value
editor.plugin(MyPlugin).store.set({ count: 5 });
// Update the value based on the previous state
editor.plugin(MyPlugin).store.set((state) => {
state.count += 1;
});In React components, use usePluginStore to subscribe to store changes:
const MyComponent = () => {
const count = usePluginStore(MyPlugin, "count");
return <div>Count: {count}</div>;
};const MyComponent = () => {
const count = usePluginStore(MyPlugin, "count");
return <div>Count: {count}</div>;
};See more in Plugin Context and Editor Methods guides.
Declare required plugins with their plugin objects. Plate installs the dependency graph recursively, deduplicates plugins by name, and loads dependencies before their dependents.
const MyPlugin = definePlatePlugin("myPlugin", {
dependencies: [ParagraphPlugin, ListPlugin],
});const MyPlugin = definePlatePlugin("myPlugin", {
dependencies: [ParagraphPlugin, ListPlugin],
});The enabled property allows you to conditionally enable or disable a plugin:
const MyPlugin = definePlatePlugin("myPlugin", {
enabled: true, // or false to disable
});const MyPlugin = definePlatePlugin("myPlugin", {
enabled: true, // or false to disable
});Keep optional capabilities as ordinary plugins in the consumer's plugin array. When an enhancement needs a host, the enhancement depends on the host:
const CodeHighlightPlugin = definePlatePlugin("codeHighlight", {
dependencies: [CodeBlockPlugin],
});
const plugins = [CodeBlockPlugin, CodeHighlightPlugin];const CodeHighlightPlugin = definePlatePlugin("codeHighlight", {
dependencies: [CodeBlockPlugin],
});
const plugins = [CodeBlockPlugin, CodeHighlightPlugin];Omit CodeHighlightPlugin for plain code blocks. The host does not install
optional enhancements.
Dependencies load before their dependents. Independent plugins keep the order provided by the application:
const plugins = [LinkPlugin, MentionPlugin, CommentPlugin];const plugins = [LinkPlugin, MentionPlugin, CommentPlugin];Use dependencies for a real installation requirement. Competing shortcuts,
input rules, and codecs own their local priority; plugin registration has no
global priority.
Use hooks on the plugin's 'text/html' codec to decide whether it participates
in an HTML input, transform the source string, or transform the decoded
fragment.
const HtmlCleanupPlugin = definePlatePlugin("htmlCleanup", {
codecs: ({ defineCodecs }) =>
defineCodecs({
"text/html": {
query: ({ data, source }) =>
source.types.includes("text/html") &&
data.includes("<!--StartFragment-->"),
transformData: ({ data }) =>
data.replaceAll(/<!--(?:Start|End)Fragment-->/g, ""),
},
}),
});const HtmlCleanupPlugin = definePlatePlugin("htmlCleanup", {
codecs: ({ defineCodecs }) =>
defineCodecs({
"text/html": {
query: ({ data, source }) =>
source.types.includes("text/html") &&
data.includes("<!--StartFragment-->"),
transformData: ({ data }) =>
data.replaceAll(/<!--(?:Start|End)Fragment-->/g, ""),
},
}),
});query, transformData, and transformFragment are siblings on the
'text/html' codec. Node-level HTML conversion remains separate from these
whole-input hooks.
definePlatePlugin infers one exact definition from the author object. Keep
state contracts explicit with a typed initialState value or satisfies;
do not pass caller generics to the factory.
The inferred definition carries the plugin name, state, API, reads, and updates:
import { property, schema } from "platejs";
import { definePlatePlugin } from "platejs/react";
export type SnippetPluginState = {
language: string;
syntax: boolean;
syntaxPopularFirst: boolean;
};
export const SnippetPlugin = definePlatePlugin("snippet", {
api: ({ store }) => ({
getLanguage: () => store.get("language"),
getSyntaxState: () => store.get("syntax"),
}),
initialState: {
language: "typescript",
syntax: true,
syntaxPopularFirst: false,
} satisfies SnippetPluginState,
schema: {
element: schema.element.textBlock({
properties: { language: property.string() },
}),
},
update: ({ schema: { type }, store, tx }) => ({
insertCurrentLanguage: () => {
tx.nodes.insert({
type,
language: store.get("language"),
children: [{ text: "" }],
});
},
}),
});import { property, schema } from "platejs";
import { definePlatePlugin } from "platejs/react";
export type SnippetPluginState = {
language: string;
syntax: boolean;
syntaxPopularFirst: boolean;
};
export const SnippetPlugin = definePlatePlugin("snippet", {
api: ({ store }) => ({
getLanguage: () => store.get("language"),
getSyntaxState: () => store.get("syntax"),
When using typed plugins, you get full type checking and autocompletion ✨
const editor = createPlateEditor({
plugins: [SnippetPlugin],
});
// Type-safe access to state
const state = editor.plugin(SnippetPlugin).store.get();
state.language;
state.syntax;
state.syntaxPopularFirst;
// Type-safe API
editor.api.snippet.getSyntaxState();
editor.api.snippet.getLanguage();
// Type-safe updates
editor.update.snippet.insertCurrentLanguage();const editor = createPlateEditor({
plugins: [SnippetPlugin],
});
// Type-safe access to state
const state = editor.plugin(SnippetPlugin).store.get();
state.language;
state.syntax;
state.syntaxPopularFirst;
// Type-safe API
editor.api.snippet.getSyntaxState();
editor.api.snippet.getLanguage();
// Type-safe updates
editor.update.snippet.insertCurrentLanguage();See the PlatePlugin API for every plugin field.