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.
Live Playground
Section titled “Live Playground”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.
Vanilla JS preview - Angular components produce the same output
Vanilla JS preview - React components produce the same output
Without the theme, you provide your own CSS for the placeholder. This demo includes minimal placeholder styles.
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.
import { Component, signal } from '@angular/core';import { DomternalEditorComponent, DomternalToolbarComponent } from '@domternal/angular';import { Editor, Document, Paragraph, Text, Placeholder } from '@domternal/core';
@Component({ selector: 'app-editor', imports: [DomternalEditorComponent, DomternalToolbarComponent], templateUrl: './editor.html',})export class EditorComponent { editor = signal<Editor | null>(null); extensions = [ Document, Paragraph, Text, Placeholder.configure({ placeholder: 'Start typing here...', }), ];}@if (editor(); as ed) { <domternal-toolbar [editor]="ed" />}<domternal-editor [extensions]="extensions" (editorCreated)="editor.set($event)"/>import { Domternal } from '@domternal/react';import { Document, Paragraph, Text, Placeholder } from '@domternal/core';
export default function Editor() { return ( <Domternal extensions={[Document, Paragraph, Text, Placeholder.configure({ placeholder: 'Start typing here...' })]} > <Domternal.Toolbar /> <Domternal.Content /> </Domternal> );}import { Editor, Document, Paragraph, Text, Placeholder } from '@domternal/core';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [ Document, Paragraph, Text, Placeholder.configure({ placeholder: 'Start typing here...', }), ],});Without @domternal/theme, you need to add your own CSS for the placeholder. The extension adds a data-placeholder attribute and .is-empty class to empty nodes:
.ProseMirror .is-empty::before { content: attr(data-placeholder); float: left; color: #adb5bd; pointer-events: none; height: 0;}Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
placeholder | string | ((props: { node, pos }) => string) | 'Write something …' | Placeholder text, or a function that returns text per node |
showOnlyWhenEditable | boolean | true | Only show placeholder when the editor is editable |
emptyNodeClass | string | 'is-empty' | CSS class added to empty nodes |
emptyEditorClass | string | 'is-editor-empty' | CSS class added when the entire document is empty |
showOnlyCurrent | boolean | true | Only show placeholder in the currently focused text block |
includeChildren | boolean | false | Include children when checking if a node is empty |
Static placeholder
Section titled “Static placeholder”Placeholder.configure({ placeholder: 'Write something …',})Dynamic per-node placeholder
Section titled “Dynamic per-node placeholder”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...'; },})Show in all empty blocks
Section titled “Show in all empty blocks”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,})Hide in read-only mode
Section titled “Hide in read-only mode”By default, placeholders are hidden when the editor is not editable. Set showOnlyWhenEditable: false to show them regardless:
Placeholder.configure({ showOnlyWhenEditable: false,})Commands
Section titled “Commands”Placeholder does not register any commands.
Keyboard shortcuts
Section titled “Keyboard shortcuts”Placeholder does not register any keyboard shortcuts.
Input rules
Section titled “Input rules”Placeholder does not register any input rules.
Toolbar items
Section titled “Toolbar items”Placeholder does not register any toolbar items.
How it works
Section titled “How it works”Placeholder creates a ProseMirror plugin that uses the decorations prop to add node decorations to empty text blocks.
Decoration structure
Section titled “Decoration structure”For each empty text block, the plugin creates a Decoration.node() with:
classattribute: theemptyNodeClass(defaultis-empty), plusemptyEditorClass(defaultis-editor-empty) if the entire document is emptydata-placeholderattribute: 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.
Empty node detection
Section titled “Empty node detection”A node is considered empty when:
includeChildren: false(default): the node has no children, or has exactly one empty text childincludeChildren: true: the node’scontent.sizeis 0
Document empty detection
Section titled “Document empty detection”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.
Performance
Section titled “Performance”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.
Styling
Section titled “Styling”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 property | Description |
|---|---|
content: attr(data-placeholder) | Reads the placeholder text from the data-placeholder attribute |
float: left | Positions 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: none | Prevents the placeholder from intercepting clicks |
height: 0 | Prevents 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;}Exports
Section titled “Exports”import { Placeholder, placeholderPluginKey } from '@domternal/core';import type { PlaceholderOptions } from '@domternal/core';| Export | Type | Description |
|---|---|---|
Placeholder | Extension | The placeholder extension |
placeholderPluginKey | PluginKey | The ProseMirror plugin key |
PlaceholderOptions | TypeScript type | Options for Placeholder.configure() |
Source
Section titled “Source”@domternal/core - Placeholder.ts