Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Stream
    • Copilot
  • Comments
  • Discussion
  • Suggestions
    • Basic Blocks
      • Blockquote
      • Heading
      • Horizontal Rule
    • Callout
    • Code Block
    • Column
    • Date
    • Equation
    • Link
    • Media
    • MentionElement
    • Table
    • Table of Contents
    • Footnote
    • Details
  • 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
    • Drag & Drop
    • Navigation Feedback
    • Tabbable
    • Toolbar
    • Yjs
    • Multi SelectEditor
    • CSV
    • DOCX
    • HTML
    • Markdown

Block Placeholder

PreviousNext

Placeholder text for the active empty block.

Demo

Block Placeholder publishes a transient placeholder attribute on the active empty block. It is block-level UI state, not stored document content. Use the editor-level placeholder prop for the globally empty editor state.

Loading…
Block MenuCombobox

On This Page

FeaturesFast pathAdd the kitStyle the placeholderOwnershipManual setupAdd the pluginAdd type-specific copyVisibility rulesStylingAPI Reference
Build your editor
Production-ready AI template and reusable components.
Get all-access

Features

  • Active-block placeholder text.
  • Per-type placeholder map through placeholders.
  • Root-level filtering through query.
  • Custom placeholder styling through className.
  • Focus, read-only, composition, selection, and empty-editor guards.
Report an issue

Fast path

Add the kit

BlockPlaceholderKit configures BlockPlaceholderPlugin for paragraph blocks.

'use client';
 
import { BlockPlaceholderPlugin } from 'platejs/react';
 
export const BlockPlaceholderKit = [
  BlockPlaceholderPlugin.configure({
    initialState: {
      className:
        'before:absolute before:cursor-text before:text-muted-foreground/80 before:content-[attr(placeholder)]',
      placeholders: {
        paragraph: 'Type something...',
      },
      query: ({ path }) => path.length === 1,
    },
  }),
];
'use client';
 
import { BlockPlaceholderPlugin } from 'platejs/react';
 
export const BlockPlaceholderKit = [
  BlockPlaceholderPlugin.configure({
    initialState: {
      className:
        'before:absolute before:cursor-text before:text-muted-foreground/80 before:content-[attr(placeholder)]',
      placeholders: {
        paragraph: 'Type something...',
      },
      query: ({ path }) => path.length === 1,
    },
  }),
];
import { createEditor } from 'platejs/react';
 
import { BlockPlaceholderKit } from '@/components/editor/block-placeholder';
 
export const editor = createEditor({
  plugins: BlockPlaceholderKit,
});
import { createEditor } from 'platejs/react';
 
import { BlockPlaceholderKit } from '@/components/editor/block-placeholder';
 
export const editor = createEditor({

Style the placeholder

The registry kit uses a before: pseudo-element that reads the rendered placeholder attribute.

BlockPlaceholderPlugin.configure({
  initialState: {
    className:
      'before:absolute before:cursor-text before:text-muted-foreground/80 before:content-[attr(placeholder)]',
  },
});
BlockPlaceholderPlugin.configure({
  initialState: {
    className:
      'before:absolute before:cursor-text before:text-muted-foreground/80 before:content-[attr(placeholder)]',
  },
});

Ownership

SurfaceOwnerWhat It Does
BlockPlaceholderPluginplatejs/reactTracks the current placeholder target per mounted view and publishes rendered block attributes.
BlockPlaceholderKitRegistryConfigures the default paragraph placeholder and styling.
block-placeholder-demoRegistry exampleShows the placeholder on an empty paragraph inside a non-empty editor.
Editor placeholder propplatejs/reactCovers the globally empty editor state.

The active target belongs to each mounted editor view and never enters plugin state or document data.

Manual setup

Add the plugin

BlockPlaceholderPlugin is available from platejs/react.

import { PLUGINS } from 'platejs';
import { BlockPlaceholderPlugin, createEditor } from 'platejs/react';
 
export const editor = createEditor({
  plugins: [
    BlockPlaceholderPlugin.configure({
      initialState: {
        className:
          'before:absolute before:cursor-text before:text-muted-foreground/80 before:content-[attr(placeholder)]',
        placeholders: {
          [PLUGINS.paragraph]: 'Type something...',
        },
        query: ({ path }) => path.length === 1,
      },
    }),
  ],
});
import { PLUGINS } from 'platejs';
import { BlockPlaceholderPlugin, createEditor } from 'platejs/react';
 
export const editor = createEditor({
  plugins: [
    BlockPlaceholderPlugin.configure({
      initialState: {
        className:
          'before:absolute before:cursor-text before:text-muted-foreground/80 before:content-[attr(placeholder)]',
        placeholders: {
          [PLUGINS.paragraph]: 'Type something...',
        },
        query: ({ path }) => path.length === 1,
      },
    }),
  ],
});

Add type-specific copy

Keys in placeholders are capability names. The plugin resolves each configured name against the editor's installed registry before matching the active block type. Application code should keep using descriptors with editor.plugin.

BlockPlaceholderPlugin.configure({
  initialState: {
    placeholders: {
      [PLUGINS.paragraph]: 'Type something...',
      [PLUGINS.heading]: 'Untitled',
      [PLUGINS.blockquote]: 'Quote',
      [PLUGINS.codeBlock]: 'Code',
    },
  },
});
BlockPlaceholderPlugin.configure({
  initialState: {
    placeholders: {






Visibility rules

The plugin shows a placeholder only when every gate passes.

GateRequirement
Editor modeNot read-only and not composing.
FocusEditor is focused and has a selection.
SelectionSelection is collapsed.
Active blockeditor.read.nodes.block() returns an empty block.
Whole editorThe editor is not in its pristine single-empty-block state. Empty blocks with visible structural state, such as list metadata, still qualify.
Placeholder mapThe block type matches one entry in placeholders.
Queryquery({ editor, node, path, ...ctx }) returns true.

The default query returns true for root blocks only. The whole-editor guard uses editor.plugin(ElementStatePlugin).api.isEmpty, so only type and compiled element properties declared with role: "metadata" are treated as pristine metadata.

query: ({ path }) => path.length === 1
query: ({ path }) => path.length === 1

Use query when placeholders should skip nested content, tables, columns, or app-specific containers.

Styling

The plugin publishes two transient attributes on the target block:

PropSource
placeholderResolved string from placeholders.
classNameinitialState.className.

Use CSS that reads attr(placeholder). Tailwind arbitrary content works well for this because the placeholder text stays in the DOM attribute instead of document data.

className:
  'before:absolute before:pointer-events-none before:text-muted-foreground/80 before:content-[attr(placeholder)]'
className:
  'before:absolute before:pointer-events-none before:text-muted-foreground/80 before:content-[attr(placeholder)]'

API Reference

APIPackageUse
BlockPlaceholderPluginplatejs/reactAdds block placeholders through transient rendered attributes.
initialState.placeholdersRecord<string, string>Maps plugin names to placeholder text. Package default: {}; copied BlockPlaceholderKit configures paragraph copy.
initialState.query(context) => booleanFilters eligible blocks. Default: ({ path }) => path.length === 1.
initialState.classNamestringClass applied to the block only while its placeholder is active.
plugins: BlockPlaceholderKit,
});
[
PLUGINS
.paragraph]:
'Type something...'
,
[PLUGINS.heading]: 'Untitled',
[PLUGINS.blockquote]: 'Quote',
[PLUGINS.codeBlock]: 'Code',
},
},
});