A First Look at Wordgard, a New Text Editor
Wordgard is a new rich-text editor library from Marijn Haverbeke, with single-change transactions, corrections, facets and library-drawn selection.
Wordgard is a JavaScript library by Marijn Haverbeke, the author of ProseMirror and CodeMirror, for building rich-text editors whose documents conform to a schema; it ships an editor UI component but is not a generic, free-form WYSIWYG or HTML editor.
Maintaining a ProseMirror integration can mean mapping positions through a list of steps, or writing a “generic” command that has to check content expressions at every turn. Wordgard is the same author’s response to those complaints, built from scratch rather than grafted onto ProseMirror.
This article covers what the library changes: the change model, the removal of content constraints, the facet-based extension system and in-library selection, plus where a first release from this author sits next to ProseMirror, TipTap and Lexical.
Key Takeaways
- Wordgard was first released as 0.1.0 on 2 July 2026 under the MIT license and installs from npm as
wordgard; the author said at release that the project will stay on 0.x versions for likely at least a year. - A Wordgard transaction carries exactly one change, built from sections that keep a token range, replace it, or add or remove marks on it, so the affected range can be read directly instead of reconstructed from a step list.
- Wordgard schemas can restrict which node types a parent may contain but not their order; corrections, observer functions that return fix-up change specs, take over invariants such as rectangular tables.
- Configuration is a tree of extensions with per-value precedence and user-definable facets, copied from CodeMirror 6.
- Wordgard handles keyboard and pointer selection in the library and draws its own cursor; touch selection is left to the browser.
What Is Wordgard?
Wordgard is a rich-text editor system for content that fits a specific schema, not a drop-in WYSIWYG component and not an application. According to the System Guide, the editing surface is meant to feel like WYSIWYG, but the content and the editing actions are named by what they mean (headers, lists, emphasis) rather than by how they look (font family, paragraph indentation, bold). The library’s headline export is the Wordgard UI class. Sitting under it are the types for documents, editor state and editing actions, and most of those work with no browser in sight.
The 0.1 announcement dated 2 July 2026 states the MIT license, the npm package name wordgard, and that the source lives on the author’s Forgejo instance. The project homepage confirms the license and adds that bug reports are welcome but pull requests are not accepted. The homepage also lists schema-based documents, modular extensions, bidirectional text, structured content such as tables and nested lists, and collaborative editing as features; treat those bullets as the project’s own claims.
How Do You Set Up a Wordgard Editor?
A minimal Wordgard editor is one call to Wordgard.create with a document, a configuration and a parent element. This is the guide’s setup example:
import {Wordgard, menuBar} from "wordgard/editor"
import {fullSchema} from "wordgard/schema"
import {history} from "wordgard/history"
let editor = Wordgard.create({
doc: `<p>Starting content</p>`,
config: [
fullSchema(), // A predefined document schema
history(), // Enable the undo history
menuBar() // Show a menu
],
parent: document.body
})
The config array is the extension tree, and each of the three entries is a bundle of extensions rather than a schema object, a plugin and a widget. fullSchema() pulls in the whole set of schema elements from wordgard/schema, and its own documentation warns that the set can pick up more elements as the library gains features; the guide’s later examples use basicSchema(), which bundles a block document, paragraphs, headings, line breaks and the strong, emphasis and link marks. The doc string is parsed as HTML against that schema. The package splits into modules such as wordgard/doc, wordgard/state, wordgard/editor, wordgard/command, wordgard/history, wordgard/schema and wordgard/types, and the guide recommends TypeScript because of how tightly the pieces interlock.
How Does Wordgard’s Change Model Differ from ProseMirror’s?
In Wordgard a transaction carries exactly one change object, built from sections that keep a stretch of the document, replace it, or add or remove marks on it, so the range an edit affects can be read directly instead of reconstructed from a list of steps. In ProseMirror a transaction is an ordered list of atomic steps, each acting on the document produced by the previous one, which forces position arithmetic and range inspection to walk the chain.
The announcement’s rationale is that the CodeMirror delta format, itself derived from ShareJS, is both simpler and more capable. A change is a flat sequence over the old document. Take a document ten tokens long: adding one token at position 4 comes out as “keep 4, replace 0 with the token, keep 6”, and making positions 3 to 6 bold comes out as “keep 3, update 3 adding the mark, keep 4”. The mark-update section is Wordgard’s extension of the CodeMirror model.
This works on a tree because positions are counted in tokens. In the guide’s index system, each plot open, plot close, non-text leaf and UTF-16 character adds one to the position, position 0 sits directly before the first child, and the document node’s own open and close tokens are not counted. That lets a change splice new token sequences into the document as if it were flat, with the change-creation code taking on the job of checking that the result is still a well-formed tree.
When several changes are given to ChangeSet.create together, every position is interpreted against the original document and the library offsets them automatically. A mark change touches no content at all:
let makeStrong = ChangeSet.create(doc, {
from: 1, to: 5,
add: Strong
})
The same objects support transforming changes over each other, which is what the undo history and collaborative editing build on.
What Replaces ProseMirror’s Content Expressions?
Wordgard schemas can restrict which node types a parent may contain, and whether a block plot may be empty, but not the order in which children appear; ProseMirror’s regular-expression content expressions have no equivalent. The announcement gives two reasons: generic document-manipulation code cannot be written against arbitrary ordering constraints without checking every operation, and hard constraints block the intermediate messy states that real editing passes through.
Rules the schema cannot state are handled by corrections. A correction is a watcher tied to a node query; it runs whenever a matching node changes or turns up, and it can hand back a change spec that the library adds to the transaction. Because a correction is code, it can respect what the user is mid-way through doing rather than mechanically rejecting the shape. The guide’s example uses Correction.onChildList(Doc, ...) to insert a level-1 heading when the document does not start with one; the announcement names rectangular tables as the case ProseMirror’s expressions could never state.
Why Does Wordgard Use Facets Instead of Plugins?
Wordgard replaces ProseMirror’s plugin as the unit of configuration and precedence with a tree of fine-grained extension values, each of which can carry its own precedence. The announcement’s complaint is precise: a ProseMirror plugin bundles several hooks under one precedence position, so a plugin that needs to be high-priority for one hook and low for another cannot get both.
In the guide’s configuration section, an extension is one of three things: a value of one of the library’s built-in extension types, any object carrying an extension in its extension field, or an array holding more of the same. Explicit precedence comes from the functions in GardState.prec; within a level, tree order decides. Facets are typed extension points that any code can define, with an optional combine function to reduce inputs to one output, and compartments allow parts of a configuration to be swapped without discarding state. The word “plugin” has not disappeared: Wordgard.Plugin.define is still there, for objects that hold their own state and need to sit close to the DOM, which is how the tooltips and panels that ship with the library are built.
Selection Drawn by the Library
Wordgard handles keyboard and pointer selection in the library and hides the native caret to draw its own cursor, while the native selection highlight itself is left visible. The announcement puts this down to unreliable browser behaviour: a cursor that will not move past certain content, that lands in the wrong spot or is not painted at all, and mouse drag selection that misfires. So the library builds its own picture of how content is laid out, does its own bidirectional text handling, and places the cursor itself. The guide’s DOM sample shows a dedicated cursor-layer element overlaying the content, and the migration document says the native highlight stays because leaving it alone gives fewer problems.
As of the 0.1 announcement, touch selection is the one exception and stays native, because reimplementing it breaks the platform context menu. That line has since moved: the changelog records touch selection in 0.5.0 for positions the native selection cannot reach, and in 0.5.1 an extra cursor position at the edges of inline plots, which gives touch drag selection somewhere to stop. The announcement also frames input handling as provisional: Wordgard handles beforeinput for everything except composition and drops ProseMirror’s DOM-mutation parsing, pending real-world testing. No browser support matrix is published.
Wordgard Next to ProseMirror, TipTap and Lexical
| ProseMirror | TipTap | Lexical | Wordgard | |
|---|---|---|---|---|
| Change model | Ordered steps | Inherits ProseMirror’s | Own model | Single section-based change |
| Content shape | Regex content expressions | Inherits ProseMirror’s | Own model | Child-type sets plus corrections |
| Configuration | Plugins | Extensions over ProseMirror plugins | Own model | Facet extensions with per-value precedence |
| Selection | Browser-native | Browser-native | Own model | Library-drawn cursor, native touch |
TipTap is a framework layer over ProseMirror and inherits its core model; Lexical is Meta’s separate editor framework. Neither shares interfaces with Wordgard.
Who should wait: most teams, for now. Wordgard was first released as 0.1.0, and the package on npm has moved through several releases since; the newest entry in the changelog is 0.5.2, dated 6 September 2026, and the changelog records breaking changes in 0.2.0, 0.3.0, 0.4.0 and 0.5.0. The author expects to rethink parts of the public interface and to remain on 0.x for likely a year or more. There is no upgrade path from ProseMirror: the project’s Migrating from ProseMirror document maps each ProseMirror package to a Wordgard module and states that no interface compatibility was attempted.
Verdict
Wordgard is the first ProseMirror-lineage editor to discard steps, ordered content expressions and browser-owned selection in one design, and those three decisions are what make it worth attention rather than the author’s name. If you maintain a ProseMirror-based product, read the migration document and the guide’s Changes and Corrections sections, then prototype one awkward schema invariant as a correction; that exercise will tell you more about fit than any feature list.
FAQs
Does Wordgard include collaborative editing, or do I need to build a server?
Wordgard ships a client-side collaborative editing extension in wordgard/collab, but no server. The collab() extension tracks unconfirmed local changes; collab.sendableUpdate and collab.receive exchange updates with a central authority you implement, and collab.transformUpdate (added in 0.2.0) lets that server rebase stale updates. Corrections skip remote transactions, so give the client config and the server transform the same corrections, listed in the same order.
What is the difference between a plot and a leaf in Wordgard?
A plot is a node with content, such as a paragraph, list, table, or the document; a leaf is a node without content, such as text, an image, or a line break. They are separate classes, Plot and Leaf, and the isPlot and isLeaf properties narrow between them in TypeScript. A leaf is its own tag (type, parameter, marks), while a plot holds a tag plus a content array.
Can I create or modify Wordgard documents outside the browser, for example in Node?
Yes for the document model, no for the editor. The wordgard/doc, wordgard/state, and wordgard/types modules are designed to run without a DOM, so you can build documents, apply change sets, run corrections, and serialize to JSON server-side; wordgard/types depends only on wordgard/doc. wordgard/editor loads outside the browser but does nothing useful, and an HTML string document needs the browser parser, so pass JSON or use jsdom.
Does Wordgard support tables, and how does it keep them rectangular?
Yes. The wordgard/table module exports a tables() extension bundle that adds the table schema elements, a CellSelection type for selecting rectangles of cells, paste and drop handlers, a table menu, and tables.correction, a built-in correction that repairs tables whose cells fail to line up into a clean rectangle. Its options are headerCells, cellSpanning, and cellContent (inline or block). Merged cells use RowSpan and ColSpan marks.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k