Plugin Rules control how editor nodes respond to common user actions. Instead of overriding the editor methods, you can configure these behaviors directly on a plugin's rules
property.
This guide shows you how to use rules.break
, rules.delete
, rules.merge
, rules.normalize
, rules.selection
and rules.match
to create intuitive editing experiences.
'use client';
import * as React from 'react';
import { Plate, usePlateEditor } from 'platejs/react';
import { EditorKit } from '@/components/editor/editor-kit';
import { Editor, EditorContainer } from '@/components/ui/editor';
import { DEMO_VALUES } from './values/demo-values';
export default function Demo({ id }: { id: string }) {
const editor = usePlateEditor({
plugins: EditorKit,
value: DEMO_VALUES[id],
});
return (
<Plate editor={editor}>
<EditorContainer variant="demo">
<Editor />
</EditorContainer>
</Plate>
);
}
Actions
Plugin rules use specific action names to define behavior:
'default'
: Default Slate behavior.'reset'
: Changes the current block to a default paragraph, keeping content.'exit'
: Exits the current block, inserting a new paragraph after it. See Exit Break to learn more about this behavior.'deleteExit'
: Deletes content then exits the block.'lineBreak'
: Inserts a line break (\n
) instead of splitting the block.
default
Standard Slate behavior. For rules.break
, splits the block. For rules.delete
, merges with the previous block.
<p>
Hello world|
</p>
After pressing Enter
:
<p>Hello world</p>
<p>
|
</p>
After pressing Backspace
:
<p>Hello world|</p>
reset
Converts the current block to a default paragraph while preserving content. Custom properties are removed.
<h3 listStyleType="disc">
|
</h3>
After pressing Enter
with rules: { break: { empty: 'reset' } }
:
<p>
|
</p>
exit
Exits the current block structure by inserting a new paragraph after it.
<blockquote>
|
</blockquote>
After pressing Enter
with rules: { break: { empty: 'exit' } }
:
<blockquote>
<text />
</blockquote>
<p>
|
</p>
deleteExit
Deletes content then exits the block.
<blockquote>
line1
|
</blockquote>
After pressing Enter
with rules: { break: { emptyLineEnd: 'deleteExit' } }
:
<blockquote>line1</blockquote>
<p>
|
</p>
lineBreak
Inserts a soft line break (\n
) instead of splitting the block.
<blockquote>
Hello|
</blockquote>
After pressing Enter
with rules: { break: { default: 'lineBreak' } }
:
<blockquote>
Hello
|
</blockquote>
rules.break
Controls what happens when users press Enter
within specific block types.
Configuration
BlockquotePlugin.configure({
rules: {
break: {
// Action when Enter is pressed normally
default: 'default' | 'lineBreak' | 'exit' | 'deleteExit',
// Action when Enter is pressed in an empty block
empty: 'default' | 'reset' | 'exit' | 'deleteExit',
// Action when Enter is pressed at end of empty line
emptyLineEnd: 'default' | 'exit' | 'deleteExit',
// If true, the new block after splitting will be reset
splitReset: boolean,
},
},
});
Each property controls a specific scenario:
-
default
-
empty
-
emptyLineEnd
-
splitReset
: Iftrue
, resets the new block to the default type after a split. This is useful for exiting a formatted block like a heading.
Examples
Reset heading on break:
import { H1Plugin } from '@platejs/heading/react';
const plugins = [
// ...otherPlugins,
H1Plugin.configure({
rules: {
break: {
splitReset: true,
},
},
}),
];
Before pressing Enter
:
<h1>
Heading|text
</h1>
After (split and reset):
<h1>
Heading
</h1>
<p>
|text
</p>
Blockquote with line breaks and smart exits:
import { BlockquotePlugin } from '@platejs/basic-nodes/react';
const plugins = [
// ...otherPlugins,
BlockquotePlugin.configure({
rules: {
break: {
default: 'lineBreak',
empty: 'reset',
emptyLineEnd: 'deleteExit',
},
},
}),
];
Before pressing Enter
in blockquote:
<blockquote>
Quote text|
</blockquote>
After (line break):
<blockquote>
Quote text
|
</blockquote>
Code block with custom empty handling:
import { CodeBlockPlugin } from '@platejs/code-block/react';
const plugins = [
// ...otherPlugins,
CodeBlockPlugin.configure({
rules: {
delete: { empty: 'reset' },
match: ({ editor, rule }) => {
return rule === 'delete.empty' && isCodeBlockEmpty(editor);
},
},
}),
];
Before pressing Backspace
in empty code block:
<code_block>
<code_line>
|
</code_line>
</code_block>
After (reset):
<p>
|
</p>
rules.delete
Controls what happens when users press Backspace
at specific positions.
Configuration
HeadingPlugin.configure({
rules: {
delete: {
// Action when Backspace is pressed at block start
start: 'default' | 'reset',
// Action when Backspace is pressed in empty block
empty: 'default' | 'reset',
},
},
});
Each property controls a specific scenario:
Examples
Reset blockquotes at start:
import { BlockquotePlugin } from '@platejs/basic-nodes/react';
const plugins = [
// ...otherPlugins,
BlockquotePlugin.configure({
rules: {
delete: { start: 'reset' },
},
}),
];
Before pressing Backspace
at start:
<blockquote>
|Quote content
</blockquote>
After (reset):
<p>
|Quote content
</p>
List items with start reset:
import { ListPlugin } from '@platejs/list/react';
const plugins = [
// ...otherPlugins,
ListPlugin.configure({
rules: {
delete: { start: 'reset' },
match: ({ rule, node }) => {
return rule === 'delete.start' && Boolean(node.listStyleType);
},
},
}),
];
Before pressing Backspace
at start of list item:
<p listStyleType="disc">
|List item content
</p>
After (reset):
<p>
|List item content
</p>
rules.merge
Controls how blocks behave when merging with previous blocks.
Configuration
ParagraphPlugin.configure({
rules: {
merge: {
// Whether to remove empty blocks when merging
removeEmpty: boolean,
},
},
});
Examples
Only paragraph and heading plugins enable removal by default. Most other plugins use false
:
import { H1Plugin, ParagraphPlugin } from 'platejs/react';
const plugins = [
// ...otherPlugins,
H1Plugin, // rules.merge: { removeEmpty: true } by default
ParagraphPlugin, // rules.merge: { removeEmpty: true } by default
];
Before pressing Backspace
at start:
<p>
<text />
</p>
<h1>
|Heading content
</h1>
After (empty paragraph removed):
<h1>
|Heading content
</h1>
Blockquote with removal disabled:
import { BlockquotePlugin } from '@platejs/basic-nodes/react';
const plugins = [
// ...otherPlugins,
BlockquotePlugin.configure({
rules: {
merge: { removeEmpty: false }, // Default
},
}),
];
Before pressing Backspace
at start:
<p>
<text />
</p>
<blockquote>
|Code content
</blockquote>
After (empty paragraph preserved):
<p>
|Code content
</p>
Table cells preserve structure during merge:
import { TablePlugin } from '@platejs/table/react';
const plugins = [
// ...otherPlugins,
TablePlugin, // Table cells have rules.merge: { removeEmpty: false }
];
Before pressing Delete
at end of paragraph:
<p>
Content|
</p>
<table>
<tr>
<td>
<p>Cell data</p>
</td>
<td>
<p>More data</p>
</td>
</tr>
</table>
After (cell content merged, structure preserved):
<p>
Content|Cell data
</p>
<table>
<tr>
<td>
<p>
<text />
</p>
</td>
<td>
<p>More data</p>
</td>
</tr>
</table>
Slate's default is true
since the default block (paragraph) is first-class, while Plate plugins are likely used to define other node behaviors that shouldn't automatically remove empty predecessors.
rules.normalize
Controls how nodes are normalized during the normalization process.
Configuration
LinkPlugin.configure({
rules: {
normalize: {
// Whether to remove nodes with empty text
removeEmpty: boolean,
},
},
});
Examples
Remove empty link nodes:
import { LinkPlugin } from '@platejs/link/react';
const plugins = [
// ...otherPlugins,
LinkPlugin.configure({
rules: {
normalize: { removeEmpty: true },
},
}),
];
Before normalization:
<p>
<a href="http://google.com">
<text />
</a>
<cursor />
</p>
After normalization (empty link removed):
<p>
<cursor />
</p>
rules.match
The match
function in plugin rules allows you to override the default behavior of specific plugins based on node properties beyond just type matching. This is particularly useful when you want to extend existing node types with new behaviors.
Examples
Code block with custom empty detection:
import { CodeBlockPlugin } from '@platejs/code-block/react';
const plugins = [
// ...otherPlugins,
CodeBlockPlugin.configure({
rules: {
delete: { empty: 'reset' },
match: ({ rule, node }) => {
return rule === 'delete.empty' && isCodeBlockEmpty(editor);
},
},
}),
];
Since the list plugin extends existing blocks that already have their own plugin configuration (e.g. ParagraphPlugin
), using rules.match
allows you to override those behaviors.
List override for paragraphs:
import { ListPlugin } from '@platejs/list/react';
const plugins = [
// ...otherPlugins,
ListPlugin.configure({
rules: {
match: ({ editor, rule }) => {
return rule === 'delete.empty' && isCodeBlockEmpty(editor);
},
},
}),
];
Custom Reset Logic
Some plugins need special reset behavior beyond the standard paragraph conversion. You can override the resetBlock
transform:
List plugin reset (outdents instead of converting to paragraph):
const ListPlugin = createPlatePlugin({
key: 'list',
// ... other config
}).overrideEditor(({ editor, tf: { resetBlock } }) => ({
transforms: {
resetBlock(options) {
if (editor.api.block(options)?.[0]?.listStyleType) {
outdentList();
return;
}
return resetBlock(options);
},
},
}));
Code block reset (unwraps instead of converting):
const CodeBlockPlugin = createPlatePlugin({
key: 'code_block',
// ... other config
}).overrideEditor(({ editor, tf: { resetBlock } }) => ({
transforms: {
resetBlock(options) {
if (editor.api.block({
at: options?.at,
match: { type: 'code_block' },
})) {
unwrapCodeBlock();
return;
}
return resetBlock(options);
},
},
}));
Combining Rules
You can combine different rules for comprehensive block behavior:
import { H1Plugin } from '@platejs/heading/react';
const plugins = [
// ...otherPlugins,
H1Plugin.configure({
rules: {
break: {
empty: 'reset',
splitReset: true,
},
delete: {
start: 'reset',
},
},
}),
];
Line break behavior (default):
<blockquote>
Hello|
</blockquote>
After Enter
:
<blockquote>
Hello
|
</blockquote>
Empty reset behavior:
<blockquote>
|
</blockquote>
After Enter
:
<p>
|
</p>
Start reset behavior:
<blockquote>
|Quote content
</blockquote>
After Backspace
:
<p>
|Quote content
</p>
Advanced
For complex scenarios beyond simple rules, you can override editor transforms directly using .overrideEditor
. This gives you complete control over transforms like resetBlock
and insertExitBreak
:
const CustomPlugin = createPlatePlugin({
key: 'custom',
// ... other config
}).overrideEditor(({ editor, tf: { insertBreak, deleteBackward, resetBlock } }) => ({
transforms: {
insertBreak() {
const block = editor.api.block();
if (/* Custom condition */) {
// Custom behavior
return;
}
// Default behavior
insertBreak();
},
deleteBackward(unit) {
const block = editor.api.block();
if (/* Custom condition */) {
// Custom behavior
return;
}
deleteBackward(unit);
},
resetBlock(options) {
if (/* Custom condition */) {
// Custom behavior
return true;
}
return resetBlock(options);
},
},
}));
rules.selection
Controls how cursor positioning and text insertion behave at node boundaries, particularly for marks and inline elements.
Configuration
BoldPlugin.configure({
rules: {
selection: {
// Define selection behavior at boundaries
affinity: 'default' | 'directional' | 'outward' | 'hard',
},
},
});
Affinity Options
The affinity
property determines how the cursor behaves when positioned at the boundary between different marks or inline elements:
default
Uses Slate's default behavior. For marks, the cursor has outward affinity at the start edge (typing before the mark doesn't apply it) and inward affinity at the end edge (typing after the mark extends it).
At end of mark (inward affinity):
<p>
<text bold>Bold text|</text><text>Normal text</text>
</p>
Typing would extend the bold formatting to new text.
At start of mark (outward affinity):
<p>
<text>Normal text|</text><text bold>Bold text</text>
</p>
Typing would not apply bold formatting to new text.
directional
Selection affinity is determined by the direction of cursor movement. When the cursor moves to a boundary, it maintains the affinity based on where it came from.
import { BoldPlugin } from '@platejs/basic-nodes/react';
const plugins = [
// ...otherPlugins,
BoldPlugin.configure({
rules: {
selection: { affinity: 'directional' },
},
}),
];
Movement from right (inward affinity):
<p>
<text>Normal</text><text bold>B|old text</text>
</p>
After pressing ←
:
<p>
<text>Normal</text><text bold>|Bold text</text>
</p>
Typing would extend the bold formatting, which is not possible with default
affinity.
import { LinkPlugin } from '@platejs/link/react';
const plugins = [
// ...otherPlugins,
LinkPlugin.configure({
rules: {
selection: { affinity: 'directional' },
},
}),
];
Movement from right (outward affinity):
<p>
Visit <a href="https://example.com">our website</a> |for more information text.
</p>
After pressing ←
:
<p>
Visit <a href="https://example.com">our website</a>| for more information text.
</p>
Cursor movement direction determines whether new text extends the link or creates new text outside it.
outward
Forces outward affinity, automatically clearing marks when typing at their boundaries. This creates a natural "exit" behavior from formatted text.
import { CommentPlugin } from '@platejs/comment/react';
const plugins = [
// ...otherPlugins,
CommentPlugin.configure({
rules: {
selection: { affinity: 'outward' },
},
}),
];
At end of marked text:
<p>
<text comment>Commented text|</text><text>Normal</text>
</p>
After typing:
<p>
<text comment>Commented text</text><text>x|Normal</text>
</p>
Users automatically exit comment formatting by typing at the end of commented text.
hard
Creates a "hard" edge that requires two key presses to move across. This provides precise cursor control for elements that need exact positioning.
import { CodePlugin } from '@platejs/basic-nodes/react';
const plugins = [
// ...otherPlugins,
CodePlugin.configure({
rules: {
selection: { affinity: 'hard' },
},
}),
];
Moving across hard edges:
<p>
<text>Before</text><text code>code|</text><text>After</text>
</p>
First →
press changes affinity:
<p>
<text>Before</text><text code>code</text>|<text>After</text>
</p>
Second →
press moves cursor:
<p>
<text>Before</text><text code>code</text><text>A|fter</text>
</p>
This allows users to position the cursor precisely at the boundary and choose whether new text should be inside or outside the code formatting.
On This Page
ActionsdefaultresetexitdeleteExitlineBreakrules.breakConfigurationExamplesrules.deleteConfigurationExamplesrules.mergeConfigurationExamplesrules.normalizeConfigurationExamplesrules.matchExamplesCustom Reset LogicCombining RulesAdvancedrules.selectionConfigurationAffinity Optionsdefaultdirectionaloutwardhard