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 { createPlateEditor } from 'platejs/react';
import { BlockPlaceholderKit } from '@/components/editor/block-placeholder';
export const editor = createPlateEditor({
plugins: BlockPlaceholderKit,
});import { createPlateEditor } from 'platejs/react';
import { BlockPlaceholderKit } from '@/components/editor/block-placeholder';
export const editor = createPlateEditor({
The registry kit uses a before: pseudo-element that reads the injected 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)]',
},
});| Surface | Owner | What It Does |
|---|---|---|
BlockPlaceholderPlugin | platejs/react / @platejs/utils/react | Tracks the current placeholder target and injects block node props. |
BlockPlaceholderKit | Registry | Configures the default paragraph placeholder and styling. |
block-placeholder-demo | Registry example | Shows the placeholder on an empty paragraph inside a non-empty editor. |
Editor placeholder prop | platejs/react | Covers the globally empty editor state. |
The plugin stores its current target in _target. That option is runtime state for rendering; do not serialize it.
BlockPlaceholderPlugin is available from platejs/react.
import { PLUGINS } from 'platejs';
import { BlockPlaceholderPlugin, createPlateEditor } from 'platejs/react';
export const editor = createPlateEditor({
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, createPlateEditor } from 'platejs/react';
export const editor = createPlateEditor({
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,
},
}),
],
});Keys in placeholders are plugin names. The plugin resolves each name through
editor.plugin(name).schema.type before matching the active block type.
BlockPlaceholderPlugin.configure({
initialState: {
placeholders: {
[PLUGINS.paragraph]: 'Type something...',
[PLUGINS.heading]: 'Untitled',
[PLUGINS.blockquote]: 'Quote',
[PLUGINS.codeBlock]: 'Code',
},
},
});BlockPlaceholderPlugin.configure({
initialState: {
placeholders: {
[
The plugin shows a placeholder only when every gate passes.
| Gate | Requirement |
|---|---|
| Editor mode | Not read-only and not composing. |
| Focus | Editor is focused and has a selection. |
| Selection | Selection is collapsed. |
| Active block | editor.read.nodes.block() returns an empty block. |
| Whole editor | The editor is not in its pristine single-empty-block state. Empty blocks with visible structural state, such as list metadata, still qualify. |
| Placeholder map | The block type matches one entry in placeholders. |
| Query | query({ 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 === 1query: ({ path }) => path.length === 1Use query when placeholders should skip nested content, tables, columns, or app-specific containers.
The plugin injects two props on the target block:
| Prop | Source |
|---|---|
placeholder | Resolved string from placeholders. |
className | initialState.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 | Package | Use |
|---|---|---|
BlockPlaceholderPlugin | platejs/react / @platejs/utils/react | Adds block placeholders through injected node props. |
initialState.placeholders | Record<string, string> | Maps plugin names to placeholder text. Package default: {}; copied BlockPlaceholderKit configures paragraph copy. |
initialState.query | (context) => boolean | Filters eligible blocks. Default: ({ path }) => path.length === 1. |
initialState.className | string | Class applied to the block only while its placeholder is active. |
initialState._target | Internal runtime state | Stores the current target node and placeholder string. |
selectors.placeholder(node) | Plugin selector | Returns the placeholder string for the current target node. |