Invisible Characters
InvisibleChars shows invisible characters like spaces (·), paragraph marks (¶), hard breaks (↵), and non-breaking spaces (°). It provides toggleInvisibleChars, showInvisibleChars, and hideInvisibleChars commands, a Mod-Shift-I keyboard shortcut, a toolbar button, and storage helpers. Each character type can be enabled or disabled individually.
Not included in StarterKit. Add it separately.
Live Playground
Section titled “Live Playground”Click the ¶ toolbar button to toggle invisible characters on and off. Spaces show as dots (·), paragraphs end with pilcrow marks (¶), and hard breaks show as ↵. You can also press Cmd/Ctrl+Shift+I.
With the default theme. The toolbar shows a toggle button that highlights when invisible characters are visible.
Vanilla JS preview - Angular components produce the same output
Vanilla JS preview - React components produce the same output
Plain editor without the theme. The button above toggles invisible characters programmatically.
import { Editor, Document, Paragraph, Text, InvisibleChars } from '@domternal/core';import '@domternal/theme';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [Document, Paragraph, Text, InvisibleChars],});
// Toggle visibilityeditor.commands.toggleInvisibleChars();
// Check current stateconsole.log(editor.storage.invisibleChars.isVisible()); // true/falseThe @domternal/theme package includes CSS for the invisible character decorations. The toolbar shows a toggle button with the ¶ icon.
import { Component, signal } from '@angular/core';import { DomternalEditorComponent, DomternalToolbarComponent } from '@domternal/angular';import { Editor, Document, Paragraph, Text, InvisibleChars } 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, InvisibleChars];}@if (editor(); as ed) { <domternal-toolbar [editor]="ed" />}<domternal-editor [extensions]="extensions" (editorCreated)="editor.set($event)"/>The toolbar auto-renders a toggle button for invisible characters with active state highlighting.
import { Domternal } from '@domternal/react';import { Document, Paragraph, Text, InvisibleChars } from '@domternal/core';
export default function Editor() { return ( <Domternal extensions={[Document, Paragraph, Text, InvisibleChars]} > <Domternal.Toolbar /> <Domternal.Content /> </Domternal> );}import { Editor, Document, Paragraph, Text, InvisibleChars } from '@domternal/core';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [ Document, Paragraph, Text, InvisibleChars.configure({ visible: true, // start visible space: true, nbsp: true, paragraph: true, hardBreak: true, }), ],});
// Toggle programmaticallyeditor.commands.toggleInvisibleChars();editor.commands.showInvisibleChars();editor.commands.hideInvisibleChars();
// Read stateeditor.storage.invisibleChars.isVisible(); // true/falseeditor.storage.invisibleChars.toggle(); // direct toggleWithout @domternal/theme, add your own CSS for the .invisible-char class and [data-char] attributes.
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
visible | boolean | false | Whether invisible characters are shown initially |
paragraph | boolean | true | Show paragraph marks (¶) at the end of paragraphs |
hardBreak | boolean | true | Show hard break marks (↵) at break positions |
space | boolean | true | Show space dots (·) on regular spaces |
nbsp | boolean | true | Show non-breaking space marks (°) on characters |
className | string | 'invisible-char' | CSS class for the decoration elements |
InvisibleChars.configure({ visible: true, // start visible paragraph: true, hardBreak: true, space: false, // hide space dots nbsp: true, className: 'my-invisible',})Character symbols
Section titled “Character symbols”| Character | Symbol | CSS class modifier |
|---|---|---|
| Paragraph end | ¶ | --paragraph |
| Hard break | ↵ | --hardBreak |
| Space | · | --space |
| Non-breaking space | ° | --nbsp |
Commands
Section titled “Commands”toggleInvisibleChars
Section titled “toggleInvisibleChars”Toggles the visibility of invisible characters.
editor.commands.toggleInvisibleChars();showInvisibleChars
Section titled “showInvisibleChars”Shows invisible characters if they are currently hidden. Does nothing if already visible.
editor.commands.showInvisibleChars();hideInvisibleChars
Section titled “hideInvisibleChars”Hides invisible characters if they are currently visible. Does nothing if already hidden.
editor.commands.hideInvisibleChars();Storage
Section titled “Storage”Access visibility helpers via editor.storage.invisibleChars:
| Method | Returns | Description |
|---|---|---|
toggle() | void | Toggles visibility directly (same as toggleInvisibleChars command) |
isVisible() | boolean | Current visibility state |
const { toggle, isVisible } = editor.storage.invisibleChars;console.log(isVisible()); // falsetoggle();console.log(isVisible()); // trueKeyboard shortcuts
Section titled “Keyboard shortcuts”| Key | Command | Description |
|---|---|---|
Mod-Shift-I | toggleInvisibleChars | Toggle invisible characters |
Mod is Cmd on macOS and Ctrl on Windows/Linux.
Input rules
Section titled “Input rules”InvisibleChars does not register any input rules.
Toolbar items
Section titled “Toolbar items”| Button | Icon | Group | Priority | Shortcut |
|---|---|---|---|---|
invisibleChars | paragraph | utility | 100 | Mod-Shift-I |
The button uses isActiveFn to read from editor.storage.invisibleChars.isVisible(), so it shows as active when invisible characters are visible.
How it works
Section titled “How it works”Decoration types
Section titled “Decoration types”InvisibleChars uses two types of ProseMirror decorations:
Widget decorations for paragraph marks and hard breaks. These insert a <span> element at the node boundary without wrapping existing content:
- Paragraph:
Decoration.widget(endPos, ...)with{ side: -1 }places¶at the end of each paragraph, before the closing tag - Hard break:
Decoration.widget(pos, ...)with{ side: -1 }places↵before the<br>element
Inline decorations for spaces and non-breaking spaces. These add a CSS class to the existing text character:
- Space:
Decoration.inline(pos, pos + 1, { class, 'data-char': 'space' })on each regular space - Nbsp:
Decoration.inline(pos, pos + 1, { class, 'data-char': 'nbsp' })on each\u00A0character
The inline decorations use ::after pseudo-elements in CSS to overlay the symbol without replacing the actual character.
Plugin state
Section titled “Plugin state”The plugin maintains state with visible and decorations:
init: Creates initial decorations ifvisibleistrue, otherwiseDecorationSet.emptyapply: Handles four cases:- Meta toggle: If the transaction has a
visiblemeta, updates state accordingly - Visibility off: Returns empty decorations immediately
- Doc changed: Rebuilds decorations from the new document
- No change: Returns the cached previous state
- Meta toggle: If the transaction has a
Toggle mechanism
Section titled “Toggle mechanism”Toggling works by dispatching a transaction with plugin meta:
const tr = state.tr.setMeta(invisibleCharsPluginKey, { visible: !currentVisible });editor.view.dispatch(tr);The plugin’s apply function reads this meta and switches between full decorations and DecorationSet.empty.
Theme CSS
Section titled “Theme CSS”The @domternal/theme package includes _invisible-chars.scss with styles for:
.invisible-char: base style with muted color,pointer-events: none,user-select: none[data-char="space"]::after: overlays·centered on the space[data-char="nbsp"]::after: overlays°centered on the non-breaking space
The color is controlled by --dm-invisible-char-color CSS variable (falls back to --dm-muted or #999).
Exports
Section titled “Exports”import { InvisibleChars, invisibleCharsPluginKey } from '@domternal/core';import type { InvisibleCharsOptions, InvisibleCharsStorage } from '@domternal/core';| Export | Type | Description |
|---|---|---|
InvisibleChars | Extension | The invisible characters extension |
invisibleCharsPluginKey | PluginKey | The ProseMirror plugin key |
InvisibleCharsOptions | TypeScript type | Options for InvisibleChars.configure() |
InvisibleCharsStorage | TypeScript type | Shape of editor.storage.invisibleChars |
Source
Section titled “Source”@domternal/core - InvisibleChars.ts