Skip to content

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.

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.

Click to try it out
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 visibility
editor.commands.toggleInvisibleChars();
// Check current state
console.log(editor.storage.invisibleChars.isVisible()); // true/false

The @domternal/theme package includes CSS for the invisible character decorations. The toolbar shows a toggle button with the icon.

OptionTypeDefaultDescription
visiblebooleanfalseWhether invisible characters are shown initially
paragraphbooleantrueShow paragraph marks () at the end of paragraphs
hardBreakbooleantrueShow hard break marks () at break positions
spacebooleantrueShow space dots (·) on regular spaces
nbspbooleantrueShow non-breaking space marks (°) on   characters
classNamestring'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',
})
CharacterSymbolCSS class modifier
Paragraph end--paragraph
Hard break--hardBreak
Space·--space
Non-breaking space°--nbsp

Toggles the visibility of invisible characters.

editor.commands.toggleInvisibleChars();

Shows invisible characters if they are currently hidden. Does nothing if already visible.

editor.commands.showInvisibleChars();

Hides invisible characters if they are currently visible. Does nothing if already hidden.

editor.commands.hideInvisibleChars();

Access visibility helpers via editor.storage.invisibleChars:

MethodReturnsDescription
toggle()voidToggles visibility directly (same as toggleInvisibleChars command)
isVisible()booleanCurrent visibility state
const { toggle, isVisible } = editor.storage.invisibleChars;
console.log(isVisible()); // false
toggle();
console.log(isVisible()); // true
KeyCommandDescription
Mod-Shift-ItoggleInvisibleCharsToggle invisible characters

Mod is Cmd on macOS and Ctrl on Windows/Linux.

InvisibleChars does not register any input rules.

ButtonIconGroupPriorityShortcut
invisibleCharsparagraphutility100Mod-Shift-I

The button uses isActiveFn to read from editor.storage.invisibleChars.isVisible(), so it shows as active when invisible characters are visible.

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 \u00A0 character

The inline decorations use ::after pseudo-elements in CSS to overlay the symbol without replacing the actual character.

The plugin maintains state with visible and decorations:

  • init: Creates initial decorations if visible is true, otherwise DecorationSet.empty
  • apply: Handles four cases:
    1. Meta toggle: If the transaction has a visible meta, updates state accordingly
    2. Visibility off: Returns empty decorations immediately
    3. Doc changed: Rebuilds decorations from the new document
    4. No change: Returns the cached previous state

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.

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).

import { InvisibleChars, invisibleCharsPluginKey } from '@domternal/core';
import type { InvisibleCharsOptions, InvisibleCharsStorage } from '@domternal/core';
ExportTypeDescription
InvisibleCharsExtensionThe invisible characters extension
invisibleCharsPluginKeyPluginKeyThe ProseMirror plugin key
InvisibleCharsOptionsTypeScript typeOptions for InvisibleChars.configure()
InvisibleCharsStorageTypeScript typeShape of editor.storage.invisibleChars

@domternal/core - InvisibleChars.ts