Character Count
CharacterCount provides character and word counting with optional limits. When a limit is set, the extension prevents transactions that would exceed it. Counts, percentages, and remaining values are available via editor.storage.characterCount.
Not included in StarterKit. Add it separately.
Live Playground
Section titled “Live Playground”This editor has a 200-character limit. Type to see the counter and progress bar update in real time. The bar turns red near the limit, and input stops when the limit is reached.
With the default theme. The counter, word count, and progress bar below the editor update on every keystroke.
Vanilla JS preview - Angular components produce the same output
Vanilla JS preview - React components produce the same output
Plain editor without the theme. The text counter below updates on every change. Input stops at the 200-character limit.
import { Editor, Document, Paragraph, Text, CharacterCount } from '@domternal/core';import '@domternal/theme';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [ Document, Paragraph, Text, CharacterCount.configure({ limit: 500 }), ],});
// Display character countconst counter = document.getElementById('counter')!;editor.on('transaction', () => { const { characters, words, remaining } = editor.storage.characterCount; counter.textContent = `${characters()} / 500 characters · ${words()} words · ${remaining()} remaining`;});Wire the transaction event to update a counter display in real time. The storage functions always return the latest values.
import { Component, signal, inject, NgZone } from '@angular/core';import { DomternalEditorComponent, DomternalToolbarComponent } from '@domternal/angular';import { Editor, Document, Paragraph, Text, CharacterCount } from '@domternal/core';
@Component({ selector: 'app-editor', imports: [DomternalEditorComponent, DomternalToolbarComponent], templateUrl: './editor.html',})export class EditorComponent { private zone = inject(NgZone); editor = signal<Editor | null>(null); charCount = signal(0); wordCount = signal(0); extensions = [Document, Paragraph, Text, CharacterCount.configure({ limit: 500 })];
onEditorCreated(editor: Editor) { this.editor.set(editor); editor.on('transaction', () => { this.zone.run(() => { this.charCount.set(editor.storage.characterCount.characters()); this.wordCount.set(editor.storage.characterCount.words()); }); }); }}@if (editor(); as ed) { <domternal-toolbar [editor]="ed" />}<domternal-editor [extensions]="extensions" (editorCreated)="onEditorCreated($event)"/><p>{{ charCount() }} / 500 characters · {{ wordCount() }} words</p>ProseMirror events fire outside Angular’s zone, so ngZone.run() is needed to trigger change detection for the counter display.
import { Domternal, useCurrentEditor, useEditorState } from '@domternal/react';import { Document, Paragraph, Text, CharacterCount } from '@domternal/core';
function CharCount() { const { editor } = useCurrentEditor(); const chars = useEditorState(editor, (ed) => ed.storage.characterCount?.characters() ?? 0); const words = useEditorState(editor, (ed) => ed.storage.characterCount?.words() ?? 0); return <p>{chars}/500 characters, {words} words</p>;}
export default function Editor() { return ( <Domternal extensions={[Document, Paragraph, Text, CharacterCount.configure({ limit: 500 })]} > <Domternal.Toolbar /> <Domternal.Content /> <CharCount /> </Domternal> );}import { Editor, Document, Paragraph, Text, CharacterCount } from '@domternal/core';
const editor = new Editor({ element: document.getElementById('editor')!, extensions: [ Document, Paragraph, Text, CharacterCount.configure({ limit: 500, wordLimit: 100 }), ],});
// Read counts at any timeconsole.log(editor.storage.characterCount.characters()); // e.g. 42console.log(editor.storage.characterCount.words()); // e.g. 7console.log(editor.storage.characterCount.remaining()); // e.g. 458console.log(editor.storage.characterCount.isLimitExceeded()); // falseOptions
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
limit | number | null | null | Maximum character count. null for no limit. When set, transactions exceeding the limit are rejected. |
wordLimit | number | null | null | Maximum word count. null for no limit. When set, transactions exceeding the limit are rejected. |
mode | 'textSize' | 'nodeSize' | 'textSize' | How to count characters |
Counting modes
Section titled “Counting modes”textSize(default) - counts only visible text characters (doc.textContent.length). This is what users see and expect.nodeSize- counts ProseMirror’s internal node size (doc.nodeSize), which includes structural characters like node boundaries. Always larger thantextSize. Useful if you want to limit total document complexity.
CharacterCount.configure({ limit: 10000, mode: 'nodeSize',})Combined limits
Section titled “Combined limits”You can set both character and word limits. A transaction is rejected if it would exceed either limit:
CharacterCount.configure({ limit: 5000, wordLimit: 500,})Storage
Section titled “Storage”Access all counting functions via editor.storage.characterCount:
| Method | Returns | Description |
|---|---|---|
characters() | number | Current character count (using configured mode) |
words() | number | Current word count (splits on whitespace, filters empty strings) |
percentage() | number | Percentage of character limit used (0-100). Returns 0 if no limit. |
remaining() | number | Characters remaining before limit. Returns Infinity if no limit. |
wordPercentage() | number | Percentage of word limit used (0-100). Returns 0 if no word limit. |
wordRemaining() | number | Words remaining before limit. Returns Infinity if no word limit. |
isLimitExceeded() | boolean | true if either character or word limit is exceeded |
const { characters, words, percentage, remaining, isLimitExceeded } = editor.storage.characterCount;
console.log(characters()); // 142console.log(words()); // 23console.log(percentage()); // 28 (if limit is 500)console.log(remaining()); // 358console.log(isLimitExceeded()); // falseWord counting algorithm
Section titled “Word counting algorithm”Words are counted by splitting doc.textContent on whitespace (/\s+/) and filtering out empty strings. This handles multiple spaces, tabs, and newlines correctly. It does not count structured elements (images, mentions) as words.
Commands
Section titled “Commands”CharacterCount does not register any commands.
Keyboard shortcuts
Section titled “Keyboard shortcuts”CharacterCount does not register any keyboard shortcuts.
Input rules
Section titled “Input rules”CharacterCount does not register any input rules.
Toolbar items
Section titled “Toolbar items”CharacterCount does not register any toolbar items.
How it works
Section titled “How it works”Storage initialization
Section titled “Storage initialization”The storage functions are initialized in the onCreate hook. They read directly from this.editor.view.state.doc on each call, so they always return the current value without caching.
Transaction filtering
Section titled “Transaction filtering”When a limit is set, the extension creates a ProseMirror plugin with filterTransaction. This function:
- Allows all non-document changes (selection changes, decoration updates, etc.) -
if (!transaction.docChanged) return true - Counts characters in the new document (
transaction.doc, not the current state) - Counts words in the new document if
wordLimitis set - Returns
false(rejects the transaction) if either count exceeds its limit - Returns
true(allows the transaction) otherwise
This means the limit is enforced before the change is applied. The editor simply stops accepting input when the limit is reached. There is no truncation or partial application.
No plugin when no limits
Section titled “No plugin when no limits”When both limit and wordLimit are null, addProseMirrorPlugins() returns an empty array. No filtering plugin is created. The storage functions still work because they are initialized in onCreate, not in the plugin.
Exports
Section titled “Exports”import { CharacterCount, characterCountPluginKey } from '@domternal/core';import type { CharacterCountOptions, CharacterCountStorage } from '@domternal/core';| Export | Type | Description |
|---|---|---|
CharacterCount | Extension | The character count extension |
characterCountPluginKey | PluginKey | The ProseMirror plugin key (only created when limits are set) |
CharacterCountOptions | TypeScript type | Options for CharacterCount.configure() |
CharacterCountStorage | TypeScript type | Shape of editor.storage.characterCount |
Source
Section titled “Source”@domternal/core - CharacterCount.ts