
Wordgard is an open-source, schema-based JavaScript rich text editor framework for building structured browser editors.
It uses schema to define which document nodes, marks, nesting rules, figures, tables, and custom content types are valid.
You can load the bundled rich text schema, compose a smaller schema from individual extensions, or define custom document elements.
The current document can be serialized to HTML or JSON for storage.
Features
- Schema-controlled document trees with custom nodes and marks.
- Bundled schemas for general rich text and inline editing.
- HTML input plus HTML and JSON serialization.
- Headings, lists, blockquotes, code blocks, links, colors, and common inline marks.
- Tables with cell selections, headers, spanning, merging, and row or column editing.
- Inline images, block figures, captions, upload hooks, alternative text, and resizing.
- Undo and redo history with configurable event grouping.
- Left-to-right, right-to-left, and automatic text direction.
- Scoped themes, light and dark schemes, and CSS custom properties.
- Translatable core, image, color, and table UI text.
- Screen-reader announcements, editor labels, and keyboard bindings.
Use Cases
- CMS and publishing editors where saved content must follow an approved document model.
- Documentation and knowledge-base editors that store structured content as HTML or JSON.
- Domain-specific authoring apps with custom blocks, marks, commands, menus, and validation rules.
- Collaborative editors with an application-owned synchronization and persistence service.
How To Use Wordgard
Installation
Install the wordgard package for an ES module or bundler-based project.
npm i wordgard
Wordgard uses style-mod, crelt, and @marijn/find-cluster-break as runtime package dependencies.
<script type="importmap">
{
"imports": {
"wordgard/editor": "https://cdn.jsdelivr.net/npm/wordgard/dist/editor.js",
"wordgard/schema": "https://cdn.jsdelivr.net/npm/wordgard/dist/schema.js",
"wordgard/state": "https://cdn.jsdelivr.net/npm/wordgard/dist/state.js",
"wordgard/doc": "https://cdn.jsdelivr.net/npm/wordgard/dist/doc.js",
"wordgard/types": "https://cdn.jsdelivr.net/npm/wordgard/dist/types.js",
"wordgard/command": "https://cdn.jsdelivr.net/npm/wordgard/dist/command.js",
"wordgard/history": "https://cdn.jsdelivr.net/npm/wordgard/dist/history.js",
"wordgard/table": "https://cdn.jsdelivr.net/npm/wordgard/dist/table.js",
"wordgard/collab": "https://cdn.jsdelivr.net/npm/wordgard/dist/collab.js",
"wordgard/phrases": "https://cdn.jsdelivr.net/npm/wordgard/dist/phrases.js",
"style-mod": "https://cdn.jsdelivr.net/npm/[email protected]/src/style-mod.js",
"crelt": "https://cdn.jsdelivr.net/npm/crelt/index.js",
"@marijn/find-cluster-break": "https://cdn.jsdelivr.net/npm/@marijn/find-cluster-break/src/index.js"
}
}
</script>
Basic Usage
Create a container for the editor:
<div id="editor"></div>
fullSchema() loads Wordgard’s standard rich text schema bundle. history() installs undo and redo, and menuBar() renders menu items contributed by the active extensions.
import {Wordgard, menuBar} from "wordgard/editor"
import {fullSchema} from "wordgard/schema"
import {history} from "wordgard/history"
const editor = Wordgard.create({
parent: document.querySelector("#editor"),
doc: `
<h2>Project Notes</h2>
<p>Start writing here.</p>
`,
config: [
fullSchema(),
history(),
menuBar()
]
})
Save Content As HTML Or JSON
serialize() converts the current document to an HTML fragment. Document nodes also expose toJSON().
import {serialize} from "wordgard/doc"
const html = serialize(editor.state.doc).toHTML()
const json = editor.state.doc.toJSON()
console.log(html)
console.log(json)
For automatic saves, create an update listener extension and include saveChanges in the editor’s config array.
import {Wordgard} from "wordgard/editor"
import {serialize} from "wordgard/doc"
const saveChanges = Wordgard.updateListener.of(update => {
if (!update.docChanged) return
const html = serialize(update.state.doc).toHTML()
const json = update.state.doc.toJSON()
console.log({html, json})
})
Build A Fixed Document Schema
fullSchema() is useful for a general-purpose editor, but its contents can grow when future Wordgard releases introduce schema elements. It does not install table editing or codeBlockLanguage().
Applications with a fixed storage contract can compose the exact schema extensions they accept:
import {Wordgard, menuBar} from "wordgard/editor"
import {
basicSchema,
bulletList,
orderedList,
blockquote,
direction,
codeBlock,
codeBlockLanguage
} from "wordgard/schema"
import {history} from "wordgard/history"
const editor = Wordgard.create({
parent: document.querySelector("#editor"),
doc: "<p>Controlled rich text content.</p>",
config: [
basicSchema(),
bulletList({blockItems: false}),
orderedList({blockItems: false}),
blockquote(),
direction(),
codeBlock(),
codeBlockLanguage({
languages: ["HTML", "CSS", "JavaScript"]
}),
history(),
menuBar()
]
})
Add Table Editing
Import tables from wordgard/table. headerCells and cellSpanning default to true. cellContent defaults to "inline".
import {Wordgard, menuBar} from "wordgard/editor"
import {basicSchema} from "wordgard/schema"
import {history} from "wordgard/history"
import {tables} from "wordgard/table"
const editor = Wordgard.create({
parent: document.querySelector("#editor"),
doc: "<p>Insert a table from the editor menu.</p>",
config: [
basicSchema(),
tables({
headerCells: true,
cellSpanning: true,
cellContent: "block"
}),
history(),
menuBar()
]
})
Configure Image Uploads
image.uploader is a Facet. Its handler receives the selected File, the editor instance, and a progress callback. Return a Promise that resolves to the uploaded image URI.
The request URL and response object in this example belong to the application:
import {Wordgard, menuBar} from "wordgard/editor"
import {basicSchema, image} from "wordgard/schema"
const imageUpload = image.uploader.of(async (file, editor, progress) => {
progress(0)
const formData = new FormData()
formData.append("file", file)
const response = await fetch("/api/images", {
method: "POST",
body: formData
})
if (!response.ok) {
throw new Error("Image upload failed")
}
const result = await response.json()
progress(100)
return result.url
})
const editor = Wordgard.create({
parent: document.querySelector("#editor"),
doc: "<p>Insert or drop an image here.</p>",
config: [
basicSchema(),
image(),
imageUpload,
menuBar()
]
})
Create A Read-Only Editor
GardState.readOnly stops editing commands and extensions from changing the document. Wordgard.editable controls the editable DOM state. Use both when the editor should act as a non-editable document viewer.
import {GardState} from "wordgard/state"
import {Wordgard, menuBar} from "wordgard/editor"
import {fullSchema} from "wordgard/schema"
const editor = Wordgard.create({
parent: document.querySelector("#editor"),
doc: "<p>This content is read-only.</p>",
config: [
fullSchema(),
GardState.readOnly.of(true),
Wordgard.editable.of(false),
menuBar()
]
})
Theme The Editor
Wordgard.theme() creates styles scoped to an editor. Wordgard.colorScheme accepts "light", "dark", or "auto" and defaults to "light".
A dark scheme selects &dark rules. Set the editor’s actual background and text colors in the theme when the application needs a dark appearance.
const editorTheme = Wordgard.theme({
"&": {
borderRadius: "8px",
backgroundColor: "#ffffff"
},
"&dark": {
backgroundColor: "#111827",
color: "#f9fafb"
}
})
const appearance = [
editorTheme,
Wordgard.colorScheme.of("auto")
]
Wordgard’s base editor styles define these CSS custom properties:
| CSS Variable | Purpose |
|---|---|
--wg-highlight-color | Selection and focus-ring color. |
--wg-dialog-font | Font used by dialogs and tooltips. |
--wg-border-color | Border color used by editor UI. |
--wg-panel-color | Background color used by panels and tooltips. |
Translate UI Text
wordgard/phrases has phrase sets for the core editor, images, color names, and tables. translate() requires every tag in a phrase set. translatePartial() accepts a subset.
Add the returned translation extensions to the editor configuration:
import {phrases, tablePhrases} from "wordgard/phrases"
const translations = [
phrases.translatePartial({
undo: "Deshacer",
redo: "Rehacer"
}),
tablePhrases.translatePartial({
insert_table: "Insertar tabla",
delete_row: "Eliminar fila",
delete_col: "Eliminar columna"
})
]
Editor Initialization Reference
Wordgard.create() accepts the complete initialization object below.
| Field | Description |
|---|---|
doc | Initial document as a Wordgard document node, JSON node, HTML string, DOM structure, or document factory. Non-document input requires a schema in config. |
selection | Initial selection. Defaults to a cursor at the start of the document. |
config | Extension tree or resolved GardState.Configuration. |
state | Existing GardState. When present, Wordgard uses this state directly. |
parent | Element or DocumentFragment that receives the editor on creation. |
scrollTo | Initial effect created with Wordgard.scrollIntoView(). |
Package Modules
The package exposes these browser-facing subpath modules:
| Module | Purpose |
|---|---|
wordgard/doc | Document nodes, parsing, serialization, positions, slices, and changes. |
wordgard/types | Standard document node and mark types. |
wordgard/schema | Standard schema elements and editing extensions. |
wordgard/table | Table schema integration, cell selections, commands, and table UI. |
wordgard/state | Editor state, selections, transactions, fields, facets, and configuration. |
wordgard/editor | Browser editor, DOM integration, menus, themes, key bindings, decorations, dialogs, and panels. |
wordgard/command | Command abstractions and editing commands. |
wordgard/history | Undo and redo history. |
wordgard/collab | Versioned collaboration state and change transformation. |
wordgard/phrases | UI phrase sets and translation helpers. |
Schema Extension Reference
The top-level wordgard/schema module exports these schema builders and helpers:
| API | Purpose |
|---|---|
blockDoc() | Root document type for block content. |
inlineDoc() | Root document type for one inline text block. |
paragraph() | Paragraph blocks and their editing controls. |
heading() | Heading blocks and heading controls. |
alignment() | Start, center, and end block alignment. |
direction() | "ltr", "rtl", and "auto" text direction. |
blockquote() | Blockquote content. |
horizontalRule() | Horizontal rules. |
bulletList() | Bullet lists with block or inline list items. |
orderedList() | Ordered lists with block or inline list items. |
codeBlock() | Code block content and editing behavior. |
codeBlockLanguage() | Optional language metadata and language selection for code blocks. |
lineBreak() | Hard line breaks. |
strong() | Strong text mark. |
emphasis() | Emphasis mark. |
code() | Inline code mark. |
underline() | Underline mark. |
strikethrough() | Strikethrough mark. |
superscript() | Superscript mark. |
subscript() | Subscript mark. |
color() | Text-color mark and picker. |
backgroundColor() | Text-background-color mark and picker. |
ColorPicker | Color picker UI and its configuration Facets. |
link() | Link mark and link editing controls. |
image() | Inline image content, image dialog, alternative text, and upload handling. |
figure() | Block-level image figures with optional captions. |
imageResizing() | Image-size mark plus drag and keyboard resizing. |
basicMarks() | strong(), emphasis(), and link(). |
inlineMarks() | The standard inline formatting mark bundle. |
basicSchema() | Block document, basic marks, paragraphs, headings, and line breaks. |
inlineSchema() | Inline document, basic marks, images, and line breaks. |
fullSchema() | Standard block schema bundle with block content, lists, inline marks, images, figures, and image resizing. |
Schema Configuration
bulletList({ blockItems }):blockItemsdefaults totrue. Set it tofalsefor inline-content list items.orderedList({ blockItems }): Uses the equivalentblockItemsconfiguration for ordered lists.codeBlockLanguage({ languages }): Accepts an optional array of language names. Stored language values use lowercase text.figure({ captioned }): Setcaptionedtotrueto enable captioned figures.image.uploader.of(handler): Registers an asynchronous image upload handler.ColorPicker.width.of(number): Sets the picker width in color cells. The default is10.ColorPicker.options.of(options): Replaces the color choices. The default collection contains 80 entries and begins with a clear-color entry.
Menu Bar Configuration
menuBar() accepts one optional field:
template(Menu.Template | readonly Menu.Template[]): Replaces the resolved default menu layout.
const customMenu = menuBar({
template: customTemplate
})
History Configuration And API
history() accepts three settings:
minDepth(number, default100): Minimum number of history events retained.newGroupDelay(number, default500): Time in milliseconds used when grouping adjacent changes.joinToEvent((transaction, isAdjacent) => boolean): Controls whether a transaction joins the current history event.
Public runtime exports:
| API | Purpose |
|---|---|
history() | Installs undo/redo history, commands, and menu controls. |
history.field | State field that stores history data. |
history.isolate | Transaction annotation for "before", "after", or two-sided isolation with true. |
history.invertedEffects | Registers inverse transaction effects for history. |
undo | Undo command. |
redo | Redo command. |
undoDepth(state) | Returns the number of undoable events. |
redoDepth(state) | Returns the number of redoable events. |
undoButton | Undo menu button. |
redoButton | Redo menu button. |
Table Configuration And API
tables() accepts the complete table configuration object:
headerCells(boolean, defaulttrue): Enables header cells.cellSpanning(boolean, defaulttrue): Enables row and column spanning.cellContent("inline" | "block", default"inline"): Selects the content model used inside cells.
Top-level table exports used for table editing:
| API | Purpose |
|---|---|
tables() | Installs table schema elements, table corrections, selection behavior, paste/drop handling, and table menu items. |
tables.correction | Corrects malformed rectangular table structures. |
tables.pasteHandler | Handles table-aware paste operations. |
tables.dropHandler | Handles table-cell move operations. |
tableMenu() | Installs table creation and modification menu items. |
CellSelection | Selection type for one or more table cells. |
addColumn("before" | "after") | Inserts a column beside the current table selection. |
addRow("before" | "after") | Inserts a row beside the current table selection. |
deleteColumn | Deletes selected columns. |
deleteRow | Deletes selected rows. |
toggleHeaderCell | Switches selected cells between header and regular cells. |
mergeCells | Merges a multi-cell selection. |
splitCell | Splits a merged cell. |
handleTablePaste | Low-level table-aware paste helper. |
Collaboration Configuration And API
wordgard/collab manages local and remote versioned changes. The application remains responsible for network transport, persistence, authentication, and the server-side document history required for advanced update transformation.
collab() accepts:
startVersion(number, default0): Starting synchronized document version.clientID(string): Client identifier. Wordgard generates one when omitted.corrections(readonly Correction[]): Corrections applied to transformed changes. Server and clients must use matching correction sets and order.sharedEffects((transaction) => readonly Transaction.Effect[]): Selects transaction effects sent with document changes.
A collaboration update contains version, clientID, changes, and optional effects.
| API | Purpose |
|---|---|
collab(config) | Installs collaboration state. |
collab.receive(state, updates) | Creates a transaction for updates received from the synchronization authority. |
collab.sendableUpdate(state) | Returns the local update ready to send, or null. |
collab.hasUnsentUpdate(state) | Checks for pending local changes. |
collab.getSyncedVersion(state) | Returns the synchronized document version. |
collab.getClientID(state) | Returns the local collaboration client ID. |
collab.transformUpdate(update, over, corrections?) | Transforms an outdated update over newer server-side changes. |
Editor Configuration APIs
Extension Helpers
| API | Purpose |
|---|---|
Wordgard.label(value) | Sets the editable element’s aria-label. |
Wordgard.theme(spec) | Creates editor-scoped theme rules. |
Wordgard.styles(spec) | Registers styles that share the editor’s base style scope. |
Wordgard.scrolling(height) | Sets an editor height and overflow scrolling. |
Facets
Use .of(value) when adding these to an editor configuration.
| Facet | Purpose |
|---|---|
Wordgard.editable | Controls the editable state of the content DOM. Default: true. |
Wordgard.colorScheme | Selects "light", "dark", or "auto". Default: "light". |
Wordgard.cspNonce | Sets the CSP nonce used for generated styles. |
Wordgard.contentAttributes | Adds attributes to the editable content element. |
Wordgard.editorAttributes | Adds attributes to the outer editor element. |
Wordgard.transactionListener | Runs synchronously when transactions are applied. |
Wordgard.updateListener | Runs after an editor update has been flushed to the DOM. |
Wordgard.cursorBlinkRate | Sets the cursor blink cycle in milliseconds. Default: 1200. |
Wordgard.exceptionSink | Receives exceptions caught in extension callbacks. |
GardState.readOnly | Prevents editing commands and extensions from changing the document. Default: false. |
Transaction Effects And Helpers
| API | Purpose |
|---|---|
Wordgard.announce.of(text) | Adds a screen-reader announcement to a transaction. |
Wordgard.scrollIntoView(position, options?) | Creates an effect that scrolls a document position or selection into view. |
Phrase Sets
wordgard/phrases exports four built-in phrase sets:
| Phrase Set | UI Text |
|---|---|
phrases | Core editor controls, formatting, history, alignment, and direction. |
imagePhrases | Image and figure dialogs, uploads, sizes, and alternative text. |
colorNames | Color names and shade descriptions. |
tablePhrases | Table creation and editing controls. |
Each PhraseSet exposes:
get(state, tag, ...insert): Resolves a phrase for the current state.ref(tag): Creates a phrase reference.translate(phrases): Creates a complete translation extension.translatePartial(phrases): Creates a partial translation extension.PhraseSet.define(phrases): Defines a new phrase set.PhraseSet.didChange(a, b): Checks whether phrase configuration changed between two states.
Alternatives And Related Resources
- 10 Best Free JavaScript WYSIWYG Rich Text Editors (2026 Update)
- SunEditor: Minimal WYSIWYG Editor In Pure JavaScript
- Notion-Like Vanilla JS WYSIWYG Editor – Redactix
- Rich Text WYSIWYG Editor for React & Vanilla JS – Editium







