@richscripts/richtexteditor
| Install | |
|---|---|
npm install @richscripts/richtexteditor |
|
| Latest Version: | 2.15.0 |
| Vue: | 3 |
| License: | Custom |
| Last Updated: | Sep 24, 2026 |
| Links: | GitHub · npm |
Rich Text Editor
Rich Text Editor is a browser-based WYSIWYG editor with toolbar customization, plugins, localization, image editing, PDF export, and classic HTML APIs.
What's in the box
A full collaborative-document workspace, not just a textarea replacement:
- AI Toolkit — in-editor proofread / rewrite / summarize / translate / chat, with streaming responses, an inline preview drawer, and a persistent accept/reject review ledger. Bring your own provider (OpenAI / Anthropic / Azure OpenAI / Groq / Ollama / custom) via
editor.aiToolkit.setResolver()or the server-sideIRichTextBoxAiResolverinterface. - Per-node Yjs CRDT engine (new in 2.2) —
editor.collab.attach({ doc, provider, textSync: "crdt" })activates character-level concurrent typing on the same paragraph with deterministic merge resolution, presence cursors viaY.RelativePosition-encoded awareness, and per-authorY.UndoManager(Ctrl-Z undoes only your edits). Pairs with any Yjs provider you self-host. Ships in the base license. - Slash commands & mentions —
/at the start of a line opens a command palette;@and#open mention pickers viaeditor.slashCommands.register()andeditor.mentions.register(). - Comments & tracked changes — threaded inline comments on text selections and block/media content such as images or tables (
editor.comments.add/reply/resolve), tracked-changes mode, per-author colors and labels. - Comments-only review mode — set
commentsOnly: trueor calleditor.setCommentsOnly(true)to lock document edits while still allowing inline comments and review discussion. - Restricted editing templates — set
restrictedEditingMode: trueto lock the document except for spans taggeddata-rte-editable="true". Mark fillable regions at runtime witheditor.restrictedEditing.markSelection(); toggle the lock viaeditor.restrictedEditing.enable()/disable()/goToNext(). Use it for forms, contracts, fillable templates — only the marked regions accept input. - Revision history — named snapshots, diff view, restore (
editor.revisionHistory.snapshot/diff/restore). - Dictation — Web Speech API integration (
editor.dictation.toggle()) with multi-language support. - DOCX import + export — import Word documents through
editor.aiToolkit.importDocx({ file?, mode? })and generate deterministic HTML → .docx viaeditor.aiToolkit.exportDocx({ filename }); preserves headings, lists, tables, images. - Structured content bridge —
editor.getJSON()/setJSON(),fromMarkdown()/toMarkdown(),renderHTML(),validateStructuredContent()— for SSR, JSON storage, and Markdown round-trip. The packaged schema now preserves merge fields, ordered-list marker styles (A/a/I/i), table-of-contents blocks, footnotes, and editor page breaks instead of collapsing them into opaque HTML. - Accessibility audit helper —
RichTextEditor.auditAccessibility()/auditAccessibility()catches empty headings, skipped heading levels, missing image alt text, and tables without header cells in browser or Node workflows. Tables markedrole="presentation"are exempt from the header rule and checked for the opposite fault instead — data-table markup on a layout table. - In-editor accessibility repair panel — the packaged
accessibilitycheckerplugin adds a side panel (editor.accessibilityChecker.open/close/toggle/run/repair) with focused fixes for missing image alt text, empty headings, skipped heading levels, headerless tables, unmarked language runs, and layout tables that still carry header cells. UseaccessibilityCheckerAutoOpento keep the workflow visible during review passes. - Layout tables (new in 2.6) — a
<table>that only positions content is not a data table, and marking it as one makes a screen reader announce relationships that do not exist.editor.setTableLayoutMode("layout")writesrole="presentation"and strips<th>/scope/<caption>;insertLayoutTable(rows, cols)starts one for an email or signature layout. Nested data tables keep their headers. - Crash recovery (new in 2.6) —
richtexteditor/rte-watchdog.jssnapshots the document, notices when the editing surface has actually died, recreates the instance and restores the text. It ignores errors from unrelated page code, and stops after repeated crashes instead of looping —crashedPermanentlyhands you the last good snapshot. - Shadow DOM (new in 2.6) — the packaged
shadowdomplugin copies the editor's own stylesheets into the shadow root that contains it. Without it the editor edits correctly and renders completely unstyled there, because no document stylesheet crosses a shadow boundary. - Restricted editing templates — the packaged
restrictededitingplugin lets apps lock the document root while keeping marked islands editable (editor.restrictedEditing.enable/disable/toggle/list/goToNext/goToPrevious). Mark reusable fields withdata-rte-editablein template HTML or calleditor.restrictedEditing.markSelection({ label })while authoring. - Document outline + TOC helpers —
RichTextEditor.getDocumentOutline()/getDocumentOutline()extracts a flat heading outline (id,text,level,path), whilegetTableOfContents()returns a nested tree andsyncTableOfContents()repopulates packaged TOC blocks from live headings. The same TOC helpers can also persist TinyMCE-style display settings such asordered,orderedListType, andincludeTitlefor numbered or titleless navigation blocks. TOC items can also expose optionalitemIndex/indexLabelmetadata viaindexMode: "linear" | "hierarchical"so custom sidebars can mirror Tiptap-style numbered navigation without reparsing headings twice. - Footnote extraction helper —
RichTextEditor.getFootnotes()/getFootnotes()returns sorted{ id, number, refId, text, path }metadata from HTML or structured JSON so review sidebars and DOCX import screens can inspect Word-style citations without reparsing the document. - Live document outline panel — the packaged
documentoutlineplugin adds a toggleable browser-side heading sidebar (editor.documentOutline.open/close/toggle/list/refresh) so long documents get CKEditor-style section navigation without custom app code. UsedocumentOutlineAutoOpen,documentOutlineMinLevel, anddocumentOutlineMaxLevelto tune the panel. - Packaged content minimap — the packaged
contentminimapplugin adds a TinyMCE/CKEditor-style miniature document preview (editor.contentMinimap.open/close/toggle/refresh/isOpen) with click-and-drag viewport navigation for long documents. UsecontentMinimapAutoOpen,contentMinimapWidth, andcontentMinimapScaleto tune the panel. - Checklist / task-list Markdown —
- [x] doneand- [ ] todoround-trip throughfromMarkdown()/toMarkdown()and render as disabled checkbox lists in HTML / SSR output. - Markdown shortcut helper parity —
getMarkdownShortcutAction()now recognizes headings, quotes, ordered/bullet/task lists, horizontal rules (---) and fenced code blocks (````` /~~~lang) so custom wrappers can mirror the same typing affordances exposed by current editor competitors. - Word + character statistics —
editor.getTextStatistics()/getSelectionStatistics()expose document + selection counts, and the built-in bottom bar can show characters, words, or both. - Case change commands — built-in
ucase,lcase, andtitlecasecommands let toolbars or custom integrations quickly apply Upper Case, Lower Case, or Title Case transforms without leaving the editor. - Document metrics helper —
RichTextEditor.getDocumentMetrics()/getDocumentMetrics()adds paragraph, sentence, heading, image, table, link, and estimated reading-time counts for analytics and publishing workflows. - Document page setup metadata —
RichTextEditor.getDocumentPageSetup()/setDocumentPageSetup()lets apps persist page format, margins, orientation, and export header/footer HTML in the structured-content document for print/PDF workflows. - Framework wrappers — React (
@richscripts/richtexteditor/react), Vue (@richscripts/richtexteditor/vue), Angular (@richscripts/richtexteditor/angular), Svelte (@richscripts/richtexteditor/svelte), Blazor wrapper, ASP.NET Core tag helper (<richtextbox>from the sister NuGet package). - Headless core —
@richscripts/richtexteditor/headlessmounts a chrome-less editing surface and hands you a controller (run,isActive,state,getHTML/getJSON/getMarkdown), so you can build the entire UI yourself. The Svelte binding is a thin action over it. Framework refs/components also expose the document-workflow helpers needed for outline, TOC, page-setup, metrics, statistics, and accessibility flows without manually round-tripping throughgetJSON(). - AI ghost-text autocomplete (2.3.0) — Copilot / Notion-AI-style inline completion: after a typing pause a greyed suggestion appears after the caret continuing your sentence; Tab accepts, Esc / typing dismisses. Bring-your-own resolver (
config.ghostTextResolver({ before, after, full })→ string | Promise); inert untilconfig.ghostTextand a resolver are set, so no model ships and no network call happens on its own. Serialization-safe (the suggestion is acontenteditable=falsespan the caret stays before;getHTMLCode/getJSONstrip it) and undo-friendly (accept viaexecCommand insertText). API:editor.requestGhostText()/acceptGhostText()/dismissGhostText()/hasGhostText()/setGhostTextResolver(); slash/ai complete. - Local draft autosave & crash recovery (2.3.0) — persists the document to
localStorageas you type and offers to restore the unsaved draft on the next load (Gmail/Notion "never lose your work"). Opt-in viaconfig.localDraft = true(or{ key, intervalMs, ttlMs, promptRestore }). The draft is never auto-applied (server content shows until the user clicks Restore); stale drafts are pruned; saves onbeforeunload. API:editor.saveLocalDraft()/hasLocalDraft()/getLocalDraft()/restoreLocalDraft()/clearLocalDraft(). - Copy / Download as Markdown (2.3.0) — Notion-style export surfacing the core
toMarkdown()engine: slash/copy as markdown+/download as markdown, oreditor.getMarkdown()/editor.copyAsMarkdown()(clipboard, execCommand fallback) /editor.downloadMarkdown(name?)(saves a.mdfile, filename derived from the first heading). Read-only; disable withconfig.markdownExportEnabled = false. - Typewriter & focus writing modes (2.3.0) — two distraction-free modes. Typewriter mode pins the caret line to a fixed vertical anchor and scrolls the editable as you type (iA Writer / Typora / Ulysses style); focus mode dims every block except the one the caret is in (Notion / iA Writer style). Toggle independently via slash
/typewriter+/focus modeoreditor.toggleTypewriterMode()/editor.toggleFocusMode()(plusset…/is…). Both off by default (config.typewriterModeEnabled,config.focusModeEnabled, anchor viaconfig.typewriterAnchor). Serialization-safe: purely presentational — focus state is runtime-only CSS classes, stripped aroundgetHTMLCode/getJSON; typewriter mode only scrolls. - Foldable headings (2.3.0) — Obsidian/Notion/Word-style section folding: hover a heading and click the gutter chevron (or
editor.toggleHeadingFold(h)/editor.foldAtCaret()/editor.unfoldAll(), slash/fold section) to collapse every block beneath it, up to the next heading of the same or higher level. Serialization-safe: fold state is runtime-only CSS classes (the chevron is a CSS::before), andgetHTMLCode/getJSONare wrapped to strip them — saved markup stays clean and folded content is never lost. Caret-safe. Disable withconfig.foldHeadingsEnabled = false. - Auto-embed media URLs on paste (2.3.0) — paste a bare YouTube or Vimeo URL on its own line and it becomes a responsive 16:9 embed (Notion/Medium style); also available as
editor.embedUrl(url). Injection-safe by construction: the iframesrcis rebuilt from a sanitized video id, never the raw input. Mid-paragraph and text-selected pastes stay links. Add providers viaconfig.autoEmbedResolver(url); disable withconfig.autoEmbedEnabled = false. - Interactive to-do / task lists (2.3.0) —
editor.execCommand("inserttodolist")(slash/to-do, toolbar) inserts a checklist whose items each have a clickable checkbox; clicking toggles open/done (check + strike-through), Notion/GitHub style. The done state lives indata-checkedand round-trips throughgetHTMLCode/getJSON; checkboxes arecontenteditable="false"spans (no fragile<input>s) and re-bind on load via event delegation. Enter-created items are auto-normalized to stay well-formed. - Emoji
:shortcode:autocomplete (2.3.0) — GitHub/Slack/Discord-style inline emoji entry: type a complete:shortcode:and it becomes the emoji (:fire:→ 🔥,:+1:→ 👍,:tada:→ 🎉), or type:+ a partial query for a keyboard-navigable popup (↑/↓, Enter/Tab, Esc). ~140 curated codes built in; extend or replace viaconfig.emojiShortcodes. Word-boundary gated (sopath:fire:is ignored) and inert inside code. Complements the emoji picker with fast keyboard-only entry. Disable withconfig.emojiAutocompleteEnabled = false. - Read-aloud / text-to-speech (2.3.0) —
editor.execCommand("readaloud")(slash/read aloud) opens a non-modal control bar (play/pause, stop, voice, rate) and reads the selection — or the whole document — aloud, highlighting each sentence as it is spoken (native selection, content untouched). Browser-native via the Web Speech API: no library bundled, no backend, no key. The speech-out complement to dictation; an accessibility win. API:editor.readAloud({rate,voice})/editor.stopReadAloud(); tune withconfig.readAloudRate/config.readAloudHighlight. - Mermaid diagrams (2.3.0) —
editor.execCommand("insertdiagram")(slash/diagram) authors diagrams-as-code (flowchart, sequence, gantt, pie, class, state…) with a live preview and renders to SVG. No renderer bundled — usesconfig.diagramResolver(server-side render like Kroki), else a host-pagewindow.mermaid, else a source<pre>fallback; the Mermaid source round-trips indata-diagramand re-renders viaeditor.renderDiagrams(). Double-click to edit; injection-safecontenteditableblocks. Pairs with the equation editor's same host-supplies-the-renderer model. - Inline Markdown autoformat (2.3.0) — type the closing delimiter and the span converts in place:
**bold**/__bold__→ bold,*italic*/_italic_→ italic,`code`→ inline code,~~strike~~→ strikethrough. Complements the existing block shortcuts (#,>,-,1.,---). Word-boundary gated so math (2*3), globs andsnake_caseare never reformatted; inert inside code/links; fully undoable. Disable withconfig.markdownInlineEnabled = false. - Email inline-style export (2.3.0) —
editor.getEmailHTML()returns email-client-safe HTML: curated computed styles written inline on every element (since Outlook/Gmail strip<style>and classes),class/id/contenteditable/data-rte-*removed,<script>/<style>dropped, content wrapped in a centered max-width table.{ wrap:false }for a bare fragment,{ width:N }for a custom width; tune the inlined set viaconfig.emailInlineProperties.editor.execCommand("emailexport")(slash/emailor toolbar) opens a copy-ready dialog. - Smart chips (2.3.0) — Google-Docs-style inline chips:
editor.execCommand("insertdatechip")drops an interactive date pill (picker + machine-readable value);insertchipopens a Date/Person/Link menu. Person/Link chips use the optionalconfig.chipResolver(query, type)for rich data, with a plain-input fallback. Atomic, injection-safe, round-trips throughgetHTMLCode. - Block drag-handles (2.3.0) — Notion-style ⋮⋮ handle on hover lets you drag-reorder top-level blocks. On by default; disable with
config.blockDragHandles = false. The handle never enters the saved content. - Spell check (2.3.0) —
editor.execCommand("spellcheck"). Native browser spell-check by default (zero infra); wireconfig.spellCheckResolver(text) => Promise<[{word, suggestions}]>(LanguageTool / server / LLM) for a review panel with click-to-replace suggestions. Insert from the slash menu (/spellcheck) or toolbar. - Web-bookmark preview cards (2.3.0) —
editor.execCommand("insertbookmark")turns a URL into a rich link-preview card (favicon + title + description + domain). Works with zero backend (basic card from URL + favicon) or wireconfig.bookmarkResolver(url) => Promise<{title,description,image,favicon}>for full unfurled previews. Insert from the slash menu (/bookmark) or toolbar;http(s)-only with escaped metadata andrel="noopener". - Equation editor (2.3.0) —
editor.execCommand("insertmath")opens a TeX/LaTeX dialog with a live KaTeX/MathJax preview and edit-in-place support; emits stable.rte-math-inlinemarkup that renders via whatever math renderer the host page loads. Insert from the slash menu (/equation) or toolbar. - Modern block types (2.3.0) — callouts (info / success / warning / danger / note admonition boxes), multi-column layouts (2–4 responsive columns), and toggle / collapsible blocks (native
<details>). Insert from the slash menu (/) or the toolbar; customize callout variants viaconfig.calloutTypes. Editor-native (no server), round-trips throughgetHTMLCode/getJSON. Closes the Notion / Google Docs / CKEditor block-type gap. - Paste-pipeline hardening (2.2.4) — Word lists pre-converted before the attribute scrubber strips the metadata; TOC / PAGEREF / HYPERLINK / SEQ / STYLEREF / NOTEREF / REF / FORMTEXT / FORMCHECKBOX / DATE / TIME / FILENAME / AUTHOR / TITLE / SUBJECT / INDEX / MERGEFIELD field instructions stripped even when scrubbed-bare; instruction text split across continuation
<span>runs caught with false-positive-safe guards;<style>/<script>/<head>/<meta>defense-in-depth; tool-specific class +data-*strip for Notion (notion-*,data-block-id), CKEditor (cke_*,data-cke-*), TinyMCE (mce_*,data-mce-*), Atlassian (confluence-*,data-confluence-*), and Google Docs (docs-internal-*,data-google-*); cleanup of empty<span></span>/<p></p>debris; smart-paste source toast detects Word / Google Docs / LibreOffice / Notion / Excel / Confluence. Opt-out viasmartPasteToast: false. - Smart link paste (2.2.4) — pasting a URL or email while text is selected wraps the selection in
<a href>instead of replacing it. Matches Google Docs / Notion / TipTap / ProseMirror behavior. Opt-out viasmartLinkPaste: false. - Mobile toolbar mode, classic toolbar customization, plugins, localization, image editing, PDF export, paste-from-Word fidelity, find & replace, autosave, draft recovery.
- Configurable empty-editor placeholders — set
placeholder,placeholderShowOnlyWhenEditable, andplaceholderColorto match the guided empty-state UX offered by current Tiptap and TinyMCE setups. - TypeScript declarations — typed editor methods, commands, event handlers, plugin namespaces, and configuration interfaces, with strict integration compilation tests.
The structured content bridge now returns a semantic document model while still carrying compatibility HTML and plain text:
{
"type": "doc",
"version": 2,
"format": "richtexteditor-json",
"html": "<p>Hello world</p>",
"text": "Hello world",
"content": [
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Hello world" }
]
}
]
}
Asset setup
Serve the richtexteditor/ folder from your app. The React and Vue wrappers default to loading assets from /richtexteditor.
Markdown, validation, and static rendering
import {
auditAccessibility,
createStructuredContent,
fromMarkdown,
getDocumentPageSetup,
getDocumentMetrics,
getDocumentOutline,
getTableOfContents,
getTextStatistics,
renderHTML,
repairAccessibilityIssue,
setDocumentPageSetup,
syncTableOfContents,
toMarkdown,
validateStructuredContent
} from "@richscripts/richtexteditor/integrations/shared";
const documentModel = fromMarkdown("## Hello\n\nThis now round-trips through **Markdown**.");
const html = renderHTML(documentModel);
const markdown = toMarkdown(documentModel);
const pagedDocument = setDocumentPageSetup(documentModel, {
format: "Letter",
margins: { top: "1in", right: "0.75in", bottom: "1in", left: "0.75in" }
});
const pageSetup = getDocumentPageSetup(pagedDocument);
const metrics = getDocumentMetrics(documentModel);
const outline = getDocumentOutline(documentModel);
const toc = getTableOfContents(documentModel, {
minLevel: 2,
maxLevel: 4,
indexMode: "hierarchical"
});
const stats = getTextStatistics(documentModel);
const accessibility = auditAccessibility(documentModel);
const repairedDocument = repairAccessibilityIssue(documentModel, accessibility.issues[0], {
altText: "Hero illustration"
});
const validation = validateStructuredContent(documentModel);
const syncedTocDocument = syncTableOfContents(documentModel, {
ordered: true,
orderedListType: "I",
includeTitle: false
});
Structured content can also preserve document-automation nodes emitted by the editor UI:
const documentModel = createStructuredContent(
'<p>Hello <span data-rte-merge-field="Customer.Name">Customer Name</span><sup data-rte-footnote-ref="1"><a href="#note-1">[1]</a></sup></p>' +
'<div data-rte-footnotes="true"><div>Footnotes</div><ol><li id="note-1" data-rte-footnote-item="1">Imported from a template. <a href="#ref-1">↩</a></li></ol></div>'
);
console.log(documentModel.content[0].content[1].type); // "mergeField"
console.log(toMarkdown(documentModel)); // includes {{Customer.Name}} and [^1]
These helpers can now also run in Node for server-side rendering and content conversion without requiring a browser DOM.
const {
auditAccessibility,
createStructuredContent,
getDocumentMetrics,
getDocumentPageSetup,
getDocumentOutline,
getTableOfContents,
getTextStatistics,
renderHTML,
repairAccessibilityIssue,
setDocumentPageSetup,
syncTableOfContents,
toMarkdown,
validateStructuredContent
} = require("@richscripts/richtexteditor/server");
const documentModel = createStructuredContent("<div data-rte-toc=\"true\"><div>Contents</div></div><h2>Hello</h2><p><strong>Server</strong> render</p>");
const html = renderHTML(documentModel);
const markdown = toMarkdown(documentModel);
const pagedDocument = setDocumentPageSetup(documentModel, {
format: "A4",
orientation: "portrait",
margins: { top: "20mm", right: "16mm", bottom: "20mm", left: "16mm" }
});
const pageSetup = getDocumentPageSetup(pagedDocument);
const metrics = getDocumentMetrics(documentModel);
const outline = getDocumentOutline(documentModel);
const toc = getTableOfContents(documentModel, {
minLevel: 2,
maxLevel: 4,
indexMode: "hierarchical"
});
const synced = syncTableOfContents(documentModel, {
ordered: true,
orderedListType: "A",
includeTitle: false
});
const stats = getTextStatistics(documentModel);
const accessibility = auditAccessibility(documentModel);
const repairedDocument = repairAccessibilityIssue(documentModel, accessibility.issues[0], {
altText: "Server-rendered architecture diagram"
});
const validation = validateStructuredContent(documentModel);
Use @richscripts/richtexteditor/server when you only need conversion and rendering on the server. Use @richscripts/richtexteditor/integrations/shared when you also need browser asset loading and editor bootstrapping helpers.
The same server/shared helpers also expose getTextStatistics(value) for consistent word and character counts outside the browser. The React, Vue, and Angular package entry points re-export this same helper family so framework apps can import editor components and document-workflow utilities from one place.
DOCX import
Current competitors like CKEditor, TinyMCE, and Tiptap all market Word/DOCX import alongside export. Rich Text Editor now ships a matching browser helper for the client side of that flow:
await editor.aiToolkit.importDocx({
url: "/richtextbox/import/docx",
mode: "replace"
});
Pass file to skip the built-in file picker, or use mode: "insert" to insert the converted HTML at the current caret instead of replacing the whole document. The helper expects the server endpoint to return either raw HTML or JSON with an html field.
React
"use client"; // Required for your own state/callback wrapper in Next.js App Router.
import { useState } from "react";
import { RichTextEditorReact } from "@richscripts/richtexteditor/react";
export default function EditorPage() {
const [html, setHtml] = useState("<p>Hello world</p>");
return (
<RichTextEditorReact
value={html}
onChange={setHtml}
assetBasePath="/richtexteditor"
style={{ minHeight: 360 }}
/>
);
}
The /react ESM entry declares its own client boundary. A Next.js App Router
Server Component may render <RichTextEditorReact value="<p>Hello</p>" /> with
serializable props. Put useState, onChange, refs and function-valued editor
configuration in your own "use client" wrapper, as above; do not pass ordinary
callbacks across the server/client boundary. Copy the runtime to public/richtexteditor
as in the installation steps. Server-side HTML/Markdown conversion belongs in
@richscripts/richtexteditor/server, not in the React client entry.
CommonJS require("@richscripts/richtexteditor/react") remains available with
the same named API. No ssr: false workaround is needed just to render the wrapper;
the browser-only editor is created after the component mounts.
Vue
import { createApp, ref } from "vue";
import { RichTextEditorVue } from "@richscripts/richtexteditor/vue";
const app = createApp({
components: { RichTextEditorVue },
setup() {
const documentModel = ref({
type: "doc",
version: 2,
format: "richtexteditor-json",
content: [
{
type: "paragraph",
content: [{ type: "text", text: "Hello world" }]
}
]
});
return { documentModel };
}
});
<RichTextEditorVue
v-model="documentModel"
value-format="json"
asset-base-path="/richtexteditor"
style="min-height: 360px"
/>
Angular
Angular 20.3+ apps can use the packaged standalone wrapper component. The public entry point is precompiled partial-Ivy ESM, with generated Angular component metadata; consumers do not need to compile the wrapper's TypeScript source:
import { Component } from "@angular/core";
import { RichTextEditorAngularComponent } from "@richscripts/richtexteditor/angular";
@Component({
selector: "app-root",
standalone: true,
imports: [RichTextEditorAngularComponent],
template: `
<rich-text-editor
[assetBasePath]="'/assets/richtexteditor'"
[value]="documentModel"
[valueFormat]="'json'"
(change)="handleChange($event.value)"
/>
`
})
export class AppComponent {
documentModel = {
type: "doc",
version: 2,
format: "richtexteditor-json",
content: [
{
type: "paragraph",
content: [{ type: "text", text: "Hello from Angular" }]
}
]
};
handleChange(value: unknown) {
this.documentModel = value as typeof this.documentModel;
}
}
The Angular wrapper exposes the same JSON bridge and convenience instance methods as the React and Vue wrappers: focus(), getEditor(), getHTMLCode(), getJSON(), setHTMLCode(), setJSON(), and plugin accessors such as getAiToolkit() / getCollab() / getComments().
When asset loading fails, show the (error) message and offer an explicit retry:
await editorComponent.retry(). It retries a failed initialization while keeping
the current bound form value. It does not replace an editor that is already ready,
start a second concurrent load, or revive a destroyed component. Fix missing asset
paths/CSP permissions before retrying. Asset autoload waits for theme CSS as well
as core/plugins; failed script or stylesheet requests can be retried without
refreshing the whole page. Manually preloaded (autoload=false) assets remain
your application's responsibility.
The repo also includes a full Angular starter in Demos/rte-ng-ts/ that uses the same wrapper pattern.
For maintainers: npm run build:angular regenerates the ESM entry point,
declarations and source/output hash manifest. npm pack runs this step through
prepack; the build needs the declared Angular/compiler/TypeScript/esbuild dev
dependencies (or the installed Angular demo toolchain in a repository checkout).
The normal test suite rejects stale generated files. Angular dependencies are
optional peers and are not required by React/Vue/server-only consumers.
Angular forms
The wrapper implements ControlValueAccessor: import FormsModule for
[(ngModel)]="body", or ReactiveFormsModule for [formControl]="bodyControl"
and formControlName. These modules come from @angular/forms; use matching
Angular core/forms versions. Angular peers are optional for React/Vue users.
import { FormControl, ReactiveFormsModule } from "@angular/forms";
// Add ReactiveFormsModule and RichTextEditorAngularComponent to your imports.
bodyControl = new FormControl("<p>Hello</p>", {
nonNullable: true,
updateOn: "blur"
});
<rich-text-editor [formControl]="bodyControl"
[assetBasePath]="'/assets/richtexteditor'" />
HTML is the default; use valueFormat="json" for structured document values.
External writes do not echo as user changes, null resets clear the editor,
and disable() / enable() update read-only state. Pending edits are flushed
before blur so updateOn: "blur" receives the final character. The default
120 ms change coalescing also applies to form changes; set [changeDelay]="0"
for the shortest quiet period. Do not combine [value] with a form binding.
An explicit config.readOnly: true remains read-only after enable().
Use a content-aware required validator: Validators.required on the HTML
string alone accepts empty markup such as <p><br></p>. Check outstanding image
uploads before saving; valid form state is not an upload-completion guarantee.
Svelte
@richscripts/richtexteditor/svelte ships a plain Svelte action — no compiler coupling,
so it works on Svelte 3 / 4 / 5. It mounts the headless core on your element and hands back the
controller, leaving the toolbar markup entirely to you.
<script>
import { richTextEditor } from "@richscripts/richtexteditor/svelte";
let editor; // controller, once ready
let state = {}; // active formats, refreshed as the caret moves
</script>
<button disabled={!editor} aria-pressed={!!state.bold} class:is-active={state.bold}
on:mousedown|preventDefault={() => {}}
on:click={() => editor.run("bold")}>Bold</button>
<button disabled={!editor || !state.canUndo}
on:mousedown|preventDefault={() => {}}
on:click={() => editor.run("undo")}>Undo</button>
<div use:richTextEditor={{
value: "<p>Hello</p>",
onReady: (e) => (editor = e),
onUpdate: (s) => (state = s)
}}></div>
Prevent the pointer's default mousedown focus change to keep the caret, but run commands
from click so keyboard activation works too. Changing
the value you pass in pushes new content into the editor, so a store can own the document, and
the action calls destroy() for you when the node unmounts. In SvelteKit this is client-only
code — keep it in a component that renders in the browser.
Options: value, config, assetBasePath, signal, onReady, onUpdate, onChange, onError.
Updating the action's options replaces callbacks without remounting. Only a
changed bound value replaces the document; an unrelated callback/state update
does not overwrite typing. Unmounting during asset loading disposes any late
controller and does not emit ready/error callbacks into a destroyed view.
For Svelte 5 runes, use reactive controller/state variables and the modern event syntax:
<script>
import { richTextEditor } from "@richscripts/richtexteditor/svelte";
let editor = $state(null);
let state = $state({});
let html = $state("<p>Hello</p>");
</script>
<button disabled={!editor} aria-pressed={!!state.bold}
onmousedown={event => event.preventDefault()}
onclick={() => editor.run("bold")}>Bold</button>
<div use:richTextEditor={{
value: html,
onReady: controller => { editor = controller; },
onUpdate: next => { state = next; },
onChange: function () { html = this.getHTML(); }
}}></div>
<textarea aria-label="HTML output" readonly value={html}></textarea>
onChange runs with the controller as this. On unmount it can synchronously
deliver a final native edit before the core's deferred change event. Read/save the
value inside that callback; do not clear your controller reference first. A
programmatic bound-value update is not emitted again as a final user edit.
Toolbar controls should have visible names, aria-pressed for toggle state and
disabled state until ready. This is a keyboard-usable starting point, not a
complete accessibility certification for a custom toolbar.
Web component
For pages that use none of the framework bindings above — Rails, Laravel, Blazor, Django, htmx, plain HTML.
<link rel="stylesheet" href="richtexteditor/rte_theme_default.min.css">
<link rel="stylesheet" href="richtexteditor/rte_mobile.min.css">
<script src="richtexteditor/rte-config.js"></script>
<script src="richtexteditor/rte.js"></script>
<!-- Optional plugins for the full toolbar. Omit unused plugins in a modular build. -->
<script src="richtexteditor/plugins/all_plugins.min.js"></script>
<script src="richtexteditor/rte-webcomponent.js"></script>
<form method="post">
<rich-text-editor name="body" value="<p>Hello</p>"></rich-text-editor>
<button>Save</button>
</form>
Serve the page and runtime from your local HTTP server, not file://. That form
posts body=... with no value-copying glue in browsers supporting form-associated
custom elements. required and form.elements.body use ElementInternals;
older-browser hidden-input fallback supports submission, not full native validation.
Always validate and sanitize the submitted HTML on your server.
React and Vue components commit the latest native input on editor blur, even if
their normal change callback is coalesced or the core change event is still pending.
This makes an ordinary editor-to-Save-button interaction safer, but it is not an
upload-completion or persistence guarantee. For programmatic saves, read fresh
HTML through the editor ref in the save/submit handler and wait for your image
upload adapter's pending requests first. A form.submit() call skips native
validation and submit listeners; use requestSubmit() when you need that workflow.
React can still batch a parent state update when code forces focus and clicks Save
inside one synchronous function. Use the editor ref for that same-turn programmatic
snapshot; a captured value from the previous parent render is not a fresh document.
The value attribute is the default document; alternatively put initial HTML
between the tags. form.reset() restores that initial document. Set the .value
property for application updates, and await readiness before calling editor APIs:
const field = document.querySelector("rich-text-editor");
field.value = "<p>Application content</p>"; // also works before first connection
await field.ready;
console.log(field.value); // current HTML, not the original value attribute
const payload = new FormData(field.form); // name="body" participates automatically
disabled (including an enclosing disabled <fieldset>) disables editing and
excludes the value from form submission. readonly prevents editing but keeps
the submitted value. Rich-text emptiness ignores whitespace/entities and blank
paragraphs; images and other media count as content. These client checks are not
upload validation. Use the image-handling guide for a persistent upload backend
and wait for pending uploads before saving a document.
Attributes: value, name, toolbar, height, width, readonly, disabled, required,
shadow, config (JSON). Properties: .value, .editor, .ready (a Promise). Events: input,
change. It renders in the light DOM by default; shadow opts into encapsulation and relies on
the shadowdom plugin for styling.
Headless (bring your own UI)
@richscripts/richtexteditor/headless is the same core the Svelte action uses, available directly
to any framework or to plain JavaScript.
import { createHeadlessEditor } from "@richscripts/richtexteditor/headless";
const editor = await createHeadlessEditor("#host", { value: "<p>Hello</p>" });
editor.run("bold"); // dispatch a command (chainable)
editor.run("formatblock", "h1"); // block types go through formatblock
editor.isActive("bold"); // => true / false
editor.can("undo"); // is the command enabled?
editor.state(); // { bold, italic, ..., canUndo, canRedo }
editor.on("update", (s) => renderToolbar(s));
editor.getHTML(); // publish-ready markup
editor.getJSON(); // structured document model
editor.getMarkdown();
editor.getStatistics(); // { words, characters }
Block-level formats go through run("formatblock", "h1") rather than run("h1"). Assets load
from /richtexteditor unless you pass assetBasePath; pass autoload: false if the page already
loads them.
Initialization supports signal: AbortSignal. Cancel an abandoned view while
assets load without cancelling other editors that share the same asset request:
const pending = new AbortController();
const initialization = createHeadlessEditor("#host", { signal: pending.signal });
// If the view closes before initialization settles: pending.abort();
const controller = await initialization;
// Once mounted, explicitly call controller.destroy() when the view closes.
Cancellation rejects with the signal's reason (AbortError by default). It
does not destroy an already mounted editor. The React/Vue headless bindings and
Svelte action wire their own pending-initialization cancellation on unmount.
React headless bindings use the latest value and callbacks. For Vue, pass a
reactive options object if the value should follow application state:
const options = reactive({ value: "<p>Initial</p>" }); // import reactive from vue
const { mountRef, editor, state } = useRichTextEditor(options);
options.value = "<p>Loaded from the application</p>";
Classic usage
<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css" />
<script src="/richtexteditor/rte.js"></script>
<script src="/richtexteditor/plugins/all_plugins.js"></script>
<!-- Production: swap the line above for the minified bundle (~56% smaller, identical behaviour):
<script src="/richtexteditor/plugins/all_plugins.min.js"></script> -->
<div id="editor"></div>
<script>
var editor = new RichTextEditor("#editor", {
placeholder: "Write something...",
placeholderShowOnlyWhenEditable: true,
commentsOnly: false,
restrictedEditingMode: false,
accessibilityCheckerAutoOpen: false,
documentOutlineAutoOpen: false,
documentOutlineMinLevel: 1,
documentOutlineMaxLevel: 3
});
editor.setHTMLCode("<p>Hello world</p>");
console.log(editor.getJSON());
</script>
Restricted editing is meant for contract, intake, and mail-merge style templates:
<p>Hello <span data-rte-editable="true" data-rte-editable-label="Customer Name">Customer Name</span>,</p>
<p>Your case number is <span data-rte-editable="true" data-rte-editable-label="Case ID">0000</span>.</p>
editor.setHTMLCode(templateHtml);
editor.restrictedEditing.enable();
editor.restrictedEditing.goToNext();
Template authors can also mark regions interactively before switching modes:
editor.restrictedEditing.markSelection({ label: "Account manager" });
editor.restrictedEditing.markSelection({ block: true, label: "Custom notes" });
editor.restrictedEditing.enable();
In order to start using Rich Text Editor, configure or customize it further, please visit the documentation.
State inspector (developer tool)
A derived view of the document the editor is actually holding: a serializable
snapshot (node tree, selection, plugin initialization order) plus structural
checks for the faults that break documents downstream - zero-length text nodes,
empty inline wrappers, duplicate nested formatting, control characters that make
an exported .docx unreadable, unsafe URL schemes, iframe[srcdoc], orphaned
list items, images with no alt text, and unprotected atomic content.
editor.stateInspector.open(); // panel with Issues / Tree / Selection / Plugins / Log
editor.stateInspector.issues(); // [{ id, severity, message, detail }]
editor.stateInspector.snapshot(); // plain JSON - attach this to a bug report
It is not in any default toolbar. Add the button with
stateInspectorToolbarButton: true, open it from the API, or type /inspect.
Other options: stateInspectorEnabled, stateInspectorAutoOpen,
stateInspectorMaxDepth, stateInspectorLogLimit.
License
For licensing information please read Licensing Info.
Related Packages
Dependency-free, tree-shakeable rich text editor & WYSIWYG for the web — comment...
A complete rich text editor. Everything included, ready in minutes. Open-source...
Lilac - a smooth, modern WYSIWYG text editor built with pure TypeScript. Zero de...
Free open-source headless rich-text Word editor for the web. Built on Tiptap & P...
Real-time collaboration for Domternal: Yjs binding with collaborative undo/redo
Version History
| Version | Released | License |
|---|---|---|
| 2.15.0 | Custom | |
| 2.14.1 | Custom | |
| 2.14.0 | Custom | |
| 2.13.0 | Custom | |
| 2.12.1 | Custom | |
| 2.12.0 | Custom | |
| 2.11.1 | Custom | |
| 2.11.0 | Custom | |
| 2.10.0 | Custom | |
| 2.9.0 | Custom | |
| 2.8.0 | Custom | |
| 2.7.2 | Custom | |
| 2.7.1 | Custom | |
| 2.7.0 | Custom | |
| 2.6.4 | Custom | |
| 2.6.3 | Custom | |
| 2.6.2 | Custom | |
| 2.6.1 | Custom | |
| 2.6.0 | Custom | |
| 2.5.4 | Custom | |
| 2.5.3 | Custom | |
| 2.5.2 | Custom | |
| 2.5.1 | Custom | |
| 2.5.0 | Custom | |
| 2.4.2 | Custom | |
| 2.4.1 | Custom | |
| 2.4.0 | Custom |