Schema-Based JavaScript Rich Text Editor – Wordgard

Category: Text | October 3, 2026
AuthorWordgard
Last UpdateOctober 3, 2026
LicenseMIT
Views0 views
Schema-Based JavaScript Rich Text Editor – Wordgard

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 VariablePurpose
--wg-highlight-colorSelection and focus-ring color.
--wg-dialog-fontFont used by dialogs and tooltips.
--wg-border-colorBorder color used by editor UI.
--wg-panel-colorBackground 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.

FieldDescription
docInitial document as a Wordgard document node, JSON node, HTML string, DOM structure, or document factory. Non-document input requires a schema in config.
selectionInitial selection. Defaults to a cursor at the start of the document.
configExtension tree or resolved GardState.Configuration.
stateExisting GardState. When present, Wordgard uses this state directly.
parentElement or DocumentFragment that receives the editor on creation.
scrollToInitial effect created with Wordgard.scrollIntoView().

Package Modules

The package exposes these browser-facing subpath modules:

ModulePurpose
wordgard/docDocument nodes, parsing, serialization, positions, slices, and changes.
wordgard/typesStandard document node and mark types.
wordgard/schemaStandard schema elements and editing extensions.
wordgard/tableTable schema integration, cell selections, commands, and table UI.
wordgard/stateEditor state, selections, transactions, fields, facets, and configuration.
wordgard/editorBrowser editor, DOM integration, menus, themes, key bindings, decorations, dialogs, and panels.
wordgard/commandCommand abstractions and editing commands.
wordgard/historyUndo and redo history.
wordgard/collabVersioned collaboration state and change transformation.
wordgard/phrasesUI phrase sets and translation helpers.

Schema Extension Reference

The top-level wordgard/schema module exports these schema builders and helpers:

APIPurpose
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.
ColorPickerColor 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 }): blockItems defaults to true. Set it to false for inline-content list items.
  • orderedList({ blockItems }): Uses the equivalent blockItems configuration for ordered lists.
  • codeBlockLanguage({ languages }): Accepts an optional array of language names. Stored language values use lowercase text.
  • figure({ captioned }): Set captioned to true to 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 is 10.
  • 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, default 100): Minimum number of history events retained.
  • newGroupDelay (number, default 500): Time in milliseconds used when grouping adjacent changes.
  • joinToEvent ((transaction, isAdjacent) => boolean): Controls whether a transaction joins the current history event.

Public runtime exports:

APIPurpose
history()Installs undo/redo history, commands, and menu controls.
history.fieldState field that stores history data.
history.isolateTransaction annotation for "before", "after", or two-sided isolation with true.
history.invertedEffectsRegisters inverse transaction effects for history.
undoUndo command.
redoRedo command.
undoDepth(state)Returns the number of undoable events.
redoDepth(state)Returns the number of redoable events.
undoButtonUndo menu button.
redoButtonRedo menu button.

Table Configuration And API

tables() accepts the complete table configuration object:

  • headerCells (boolean, default true): Enables header cells.
  • cellSpanning (boolean, default true): 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:

APIPurpose
tables()Installs table schema elements, table corrections, selection behavior, paste/drop handling, and table menu items.
tables.correctionCorrects malformed rectangular table structures.
tables.pasteHandlerHandles table-aware paste operations.
tables.dropHandlerHandles table-cell move operations.
tableMenu()Installs table creation and modification menu items.
CellSelectionSelection 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.
deleteColumnDeletes selected columns.
deleteRowDeletes selected rows.
toggleHeaderCellSwitches selected cells between header and regular cells.
mergeCellsMerges a multi-cell selection.
splitCellSplits a merged cell.
handleTablePasteLow-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, default 0): 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.

APIPurpose
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

APIPurpose
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.

FacetPurpose
Wordgard.editableControls the editable state of the content DOM. Default: true.
Wordgard.colorSchemeSelects "light", "dark", or "auto". Default: "light".
Wordgard.cspNonceSets the CSP nonce used for generated styles.
Wordgard.contentAttributesAdds attributes to the editable content element.
Wordgard.editorAttributesAdds attributes to the outer editor element.
Wordgard.transactionListenerRuns synchronously when transactions are applied.
Wordgard.updateListenerRuns after an editor update has been flushed to the DOM.
Wordgard.cursorBlinkRateSets the cursor blink cycle in milliseconds. Default: 1200.
Wordgard.exceptionSinkReceives exceptions caught in extension callbacks.
GardState.readOnlyPrevents editing commands and extensions from changing the document. Default: false.

Transaction Effects And Helpers

APIPurpose
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 SetUI Text
phrasesCore editor controls, formatting, history, alignment, and direction.
imagePhrasesImage and figure dialogs, uploads, sizes, and alternative text.
colorNamesColor names and shade descriptions.
tablePhrasesTable 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

You Might Be Interested In:


Leave a Reply