From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Stream
    • Copilot
  • Discussion
    • Comments
    • Suggestion
    • Basic Blocks
      • Blockquote
      • Heading
      • Horizontal Rule
    • Callout
    • Code Block
    • Column
    • Date
    • Equation
    • Link
    • List Classic
    • Media
    • MentionElement
    • Table
    • Table of Contents
    • Footnote
    • Toggle
  • Marks
    • Bold
    • Italic
    • Underline
    • Code
    • Highlight
    • Keyboard Input
    • Strikethrough
    • Subscript
    • Superscript
      • Font
      • Line Height
      • Text Align
    • Indent
    • List
      • Exit Break
      • Single Block
      • Trailing Block
    • Autoformat
    • Block Menu
    • Block Placeholder
    • Combobox
      • Emoji
      • MentionElement
      • Slash Command
    • Cursor Overlay
    • Drag & Drop
    • Navigation Feedback
    • Tabbable
    • Toolbar
    • Yjs
    • Multi SelectEditor
    • CSV
    • DOCX
    • HTML
    • Markdown

Trailing Block

PreviousNext

Keep a required block at the end of an editor or nested level.

Single BlockForced Layout

Trailing Block inserts a required block when the last node at a target level is missing or has the wrong type. The standard registry editor plugins include TrailingBlockPlugin so full Plate editors always end with a paragraph. Single-block and single-line editors disable it because they intentionally keep one root block.

Features

  • Default trailing type from the editor paragraph plugin.
  • Empty-editor protection.
  • Root or nested target level.
  • Optional last-node matching through initialState.match.
  • Custom insertion wrapper through initialState.insert.
  • Included in the standard registry editor plugins.
Report an issue

Fast Path

Add when users need a safe place to continue typing after blocks such as headings, tables, media, or columns.

Single BlockAutoformat

On This Page

FeaturesFast PathOwnershipConfigure The TypeMatch The Previous BlockNested LevelCustom InsertBehaviorAPI Reference
Build your editor
Production-ready AI template and reusable components.
Get all-access
TrailingBlockPlugin
import { TrailingBlockPlugin } from 'platejs';
import { createPlateEditor } from 'platejs/react';
 
export const editor = createPlateEditor({
  plugins: [TrailingBlockPlugin],
});
import { TrailingBlockPlugin } from 'platejs';
import { createPlateEditor } from 'platejs/react';
 
export const editor = createPlateEditor({
  plugins: [TrailingBlockPlugin],
});

TrailingBlockPlugin defaults to the editor's paragraph type.

Ownership

LayerOwnerWhat It Does
TrailingBlockPluginplatejs / @platejs/utilsOwns the correction that checks and inserts the trailing block.
tx.nodes.last([], { level })Plite transactionFinds the last node at the configured depth.
NodeApi.matches(...)@platejs/pliteApplies the optional last-node match.
editor.ts pluginsRegistryAdd TrailingBlockPlugin after editing plugins.
SuggestionKitRegistryWeakly adapts an installed trailing block without installing it.

There is no dedicated trailing-block UI. The plugin is a normalizer.

Configure The Type

Use type when the trailing block should be something other than the default paragraph.

import { ElementApi, PLUGINS, TrailingBlockPlugin } from 'platejs';
 
export const trailingBlockPlugin = TrailingBlockPlugin.configure(
  ({ editor }) => ({
    initialState: {
      type: editor.plugin(PLUGINS.paragraph).schema.type,
    },
  })
);
import { ElementApi, PLUGINS, TrailingBlockPlugin } from 'platejs';
 
export const trailingBlockPlugin = TrailingBlockPlugin.configure(
  ({ editor }) => ({
    initialState: {
      type: editor.plugin(PLUGINS.paragraph).schema.type,
    },
  })
);

The default is already editor.plugin(PLUGINS.paragraph).schema.type, so most editors can use the plugin directly.

Match The Previous Block

Use match to limit insertion based on the current last block.

import { HeadingPlugin } from '@platejs/basic-nodes/react';
import { PLUGINS, TrailingBlockPlugin } from 'platejs';
 
export const trailingBlockPlugins = [
  HeadingPlugin,
  TrailingBlockPlugin.configure(({ editor }) => ({
    initialState: {
      match: (node) =>
        ElementApi.isElement(node) &&
        node.type === editor.plugin(PLUGINS.heading).schema.type,
      type: editor.plugin(PLUGINS.paragraph).schema.type,
    },
  })),
] as const;
import { HeadingPlugin } from '@platejs/basic-nodes/react';
import { PLUGINS, TrailingBlockPlugin } from 'platejs';
 
export const trailingBlockPlugins = [
  HeadingPlugin,
  TrailingBlockPlugin.configure(({ editor }) => ({
    initialState: {
      match: (node) =>
        ElementApi.isElement(node) &&
        node.type === editor.plugin(PLUGINS.heading).schema.type,
      type: editor.plugin(PLUGINS.paragraph).schema.type,
    },
  })),
] as const;

Install the referenced H1 capability beside the configured plugin. A trailing paragraph is then inserted when the last block is an H1. Without match, any last block whose type differs from the trailing type can trigger insertion.

Nested Level

level changes where the plugin looks for the last node.

levelTarget
0Last root block.
1Last child inside the last root-level container.
TrailingBlockPlugin.configure({
  initialState: {
    level: 1,
    type: 'paragraph',
  },
});
TrailingBlockPlugin.configure({
  initialState: {
    level: 1,
    type: 'paragraph',
  },
});

Use nested levels when a constrained container must always end with a text block.

Custom Insert

initialState.insert lets another plugin wrap the generated insertion. The registry suggestion kit contributes this option through override.plugins only when the editor also installs TrailingBlockPlugin.

import { SUGGESTION_SKIP_TAG } from '@platejs/suggestion';
import { TrailingBlockPlugin } from 'platejs';
 
TrailingBlockPlugin.configure({
  initialState: {
    insert: (_editor, { insert, tx }) => {
      tx.tags.add(SUGGESTION_SKIP_TAG);
      insert();
    },
  },
});
import { SUGGESTION_SKIP_TAG } from '@platejs/suggestion';
import { TrailingBlockPlugin } from 'platejs';
 
TrailingBlockPlugin.configure({
  initialState: {
    insert: (_editor, { insert, tx }) => {
      tx.tags.add(SUGGESTION_SKIP_TAG);
      insert();
    },
  },
});

The callback receives the editor, active transaction tags, insertion path, target type, and an insert() function. Call insert() exactly once unless you are intentionally replacing the default insertion.

A direct TrailingBlockPlugin.configure(...) call is the strong owner and takes precedence over the suggestion kit's weak contribution.

Behavior

CaseResult
Empty editorInserts a block at [0].
Last node already matches typeFalls through to the base normalizeNode.
Last node has another type and matches initialState.matchInserts the trailing block at PathApi.next(lastChildPath).
Last node does not match initialState.matchDoes not insert.

The correction inserts { children: [{ text: '' }], type: trailingType } through the active transaction.

API Reference

APIPackageUse
TrailingBlockPluginplatejs / @platejs/utilsNormalizer that ensures a trailing block exists.
TrailingBlockPluginState.type@platejs/utilsBlock type to insert. Defaults to the editor paragraph type.
TrailingBlockPluginState.level@platejs/utilsDepth used by tx.nodes.last. Defaults to 0.
TrailingBlockPluginState.insert@platejs/utilsCustom wrapper around the generated insertion.
TrailingBlockPluginState.match@platejs/plite node matchLimits insertion to matching last nodes.