Skip to content

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.

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.

Click to try it out
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 count
const 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.

OptionTypeDefaultDescription
limitnumber | nullnullMaximum character count. null for no limit. When set, transactions exceeding the limit are rejected.
wordLimitnumber | nullnullMaximum word count. null for no limit. When set, transactions exceeding the limit are rejected.
mode'textSize' | 'nodeSize''textSize'How to count characters
  • 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 than textSize. Useful if you want to limit total document complexity.
CharacterCount.configure({
limit: 10000,
mode: 'nodeSize',
})

You can set both character and word limits. A transaction is rejected if it would exceed either limit:

CharacterCount.configure({
limit: 5000,
wordLimit: 500,
})

Access all counting functions via editor.storage.characterCount:

MethodReturnsDescription
characters()numberCurrent character count (using configured mode)
words()numberCurrent word count (splits on whitespace, filters empty strings)
percentage()numberPercentage of character limit used (0-100). Returns 0 if no limit.
remaining()numberCharacters remaining before limit. Returns Infinity if no limit.
wordPercentage()numberPercentage of word limit used (0-100). Returns 0 if no word limit.
wordRemaining()numberWords remaining before limit. Returns Infinity if no word limit.
isLimitExceeded()booleantrue if either character or word limit is exceeded
const { characters, words, percentage, remaining, isLimitExceeded } = editor.storage.characterCount;
console.log(characters()); // 142
console.log(words()); // 23
console.log(percentage()); // 28 (if limit is 500)
console.log(remaining()); // 358
console.log(isLimitExceeded()); // false

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.

CharacterCount does not register any commands.

CharacterCount does not register any keyboard shortcuts.

CharacterCount does not register any input rules.

CharacterCount does not register any toolbar items.

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.

When a limit is set, the extension creates a ProseMirror plugin with filterTransaction. This function:

  1. Allows all non-document changes (selection changes, decoration updates, etc.) - if (!transaction.docChanged) return true
  2. Counts characters in the new document (transaction.doc, not the current state)
  3. Counts words in the new document if wordLimit is set
  4. Returns false (rejects the transaction) if either count exceeds its limit
  5. 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.

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.

import { CharacterCount, characterCountPluginKey } from '@domternal/core';
import type { CharacterCountOptions, CharacterCountStorage } from '@domternal/core';
ExportTypeDescription
CharacterCountExtensionThe character count extension
characterCountPluginKeyPluginKeyThe ProseMirror plugin key (only created when limits are set)
CharacterCountOptionsTypeScript typeOptions for CharacterCount.configure()
CharacterCountStorageTypeScript typeShape of editor.storage.characterCount

@domternal/core - CharacterCount.ts