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/style-mod@4.1.3/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
The post Schema-Based JavaScript Rich Text Editor – Wordgard appeared first on CSS Script.
Discover more from RSS Feeds Cloud
Subscribe to get the latest posts sent to your email.
