Skip to content

Placeholder

Placeholder shows placeholder text when the editor or individual text blocks are empty. It supports static text, dynamic per-node functions, and can show placeholders in all empty blocks or only the currently focused one. The placeholder is rendered via CSS ::before pseudo-element using a data-placeholder attribute.

Not included in StarterKit. Add it separately.

Click the editor and clear the text to see the placeholder appear. This demo uses a dynamic per-node placeholder that shows different text for headings and paragraphs.

With the default theme. The placeholder text appears in muted color and disappears as soon as you type.

Click to try it out
import { Editor, Document, Paragraph, Text, Placeholder } from '@domternal/core';
import '@domternal/theme';
const editor = new Editor({
element: document.getElementById('editor')!,
extensions: [
Document, Paragraph, Text,
Placeholder.configure({
placeholder: 'Start typing here...',
}),
],
});

The @domternal/theme package includes the CSS that renders the placeholder text. The placeholder appears when a text block is empty and disappears as soon as the user types.

OptionTypeDefaultDescription
placeholderstring | ((props: { node, pos }) => string)'Write something …'Placeholder text, or a function that returns text per node
showOnlyWhenEditablebooleantrueOnly show placeholder when the editor is editable
emptyNodeClassstring'is-empty'CSS class added to empty nodes
emptyEditorClassstring'is-editor-empty'CSS class added when the entire document is empty
showOnlyCurrentbooleantrueOnly show placeholder in the currently focused text block
includeChildrenbooleanfalseInclude children when checking if a node is empty
Placeholder.configure({
placeholder: 'Write something …',
})

Use a function to show different placeholder text depending on the node type or position:

Placeholder.configure({
placeholder: ({ node }) => {
if (node.type.name === 'heading') return 'Enter a heading...';
if (node.type.name === 'codeBlock') return '// Write code here';
return 'Type something...';
},
})

By default, the placeholder only appears in the currently focused text block (showOnlyCurrent: true). Set it to false to show placeholders in every empty text block at once:

Placeholder.configure({
placeholder: ({ node }) => {
if (node.type.name === 'heading') return 'Heading';
return 'Paragraph';
},
showOnlyCurrent: false,
})

By default, placeholders are hidden when the editor is not editable. Set showOnlyWhenEditable: false to show them regardless:

Placeholder.configure({
showOnlyWhenEditable: false,
})

Placeholder does not register any commands.

Placeholder does not register any keyboard shortcuts.

Placeholder does not register any input rules.

Placeholder does not register any toolbar items.

Placeholder creates a ProseMirror plugin that uses the decorations prop to add node decorations to empty text blocks.

For each empty text block, the plugin creates a Decoration.node() with:

  • class attribute: the emptyNodeClass (default is-empty), plus emptyEditorClass (default is-editor-empty) if the entire document is empty
  • data-placeholder attribute: the placeholder text (static string or function result)

The actual placeholder text is rendered via CSS using content: attr(data-placeholder) on the ::before pseudo-element. This means the placeholder is not part of the document content and does not interfere with selection or editing.

A node is considered empty when:

  • includeChildren: false (default): the node has no children, or has exactly one empty text child
  • includeChildren: true: the node’s content.size is 0

The entire document is considered empty when it has exactly one child, that child is a text block, and its content size is 0. This typically means the document contains a single empty paragraph.

When showOnlyCurrent: true (default), the plugin uses selection.$anchor to check only the node at the cursor position. This is O(1) regardless of document size.

When showOnlyCurrent: false, the plugin calls doc.descendants() to find all empty text blocks. This is O(n) where n is the number of nodes in the document.

The @domternal/theme package includes placeholder styles:

.dm-editor .ProseMirror .is-empty::before {
content: attr(data-placeholder);
float: left;
color: var(--dm-placeholder-color);
pointer-events: none;
height: 0;
}
CSS propertyDescription
content: attr(data-placeholder)Reads the placeholder text from the data-placeholder attribute
float: leftPositions the placeholder inline with the text cursor
color: var(--dm-placeholder-color)Uses the theme’s muted text color (defaults to --dm-muted)
pointer-events: nonePrevents the placeholder from intercepting clicks
height: 0Prevents the placeholder from taking up space in the layout

Customize the placeholder color with the --dm-placeholder-color CSS variable on .dm-editor:

.dm-editor {
--dm-placeholder-color: #9ca3af;
}
import { Placeholder, placeholderPluginKey } from '@domternal/core';
import type { PlaceholderOptions } from '@domternal/core';
ExportTypeDescription
PlaceholderExtensionThe placeholder extension
placeholderPluginKeyPluginKeyThe ProseMirror plugin key
PlaceholderOptionsTypeScript typeOptions for Placeholder.configure()

@domternal/core - Placeholder.ts