The Rich Text Editor
your framework
deserves
Classic and Notion-style editors out of the box.
Or go headless and assemble from 70+ tree-shakeable extensions.
First-class wrappers for Angular, React, Vue, and Vanilla.
Everything you need to build
rich editing experiences
A complete editor framework with full control over every aspect of your editor.
70+ Built-in Extensions
27 nodes, 9 marks, 37 extensions across 17 tree-shakeable packages. Tables with merge, images, emoji, mentions, details, LaTeX math, syntax highlighting, Notion-style blocks, table of contents, and Markdown import/export.
Angular, React, Vue & Vanilla Wrappers
Drop-in editor, toolbar, bubble menu, floating menu, emoji picker, and Notion color picker. Or skip them - use the headless core with any framework.
Built-in Toolbar & Theme
Each extension contributes toolbar items via addToolbarItems(). 51 Phosphor icons. Light and dark themes via CSS custom properties - restyle with one declaration.
Styled HTML Export
getHTML({ styled: true }) outputs inline CSS that survives email clients, CMS rich-text fields, and Google Docs paste. No external stylesheet required.
Markdown In & Out
Type ## for headings or / for the slash menu, paste whole Markdown documents, and export back to GitHub-flavored Markdown with @domternal/extension-markdown.
Notion-style Block UX
Hover to grab a block, drag to reorder. Right-click for duplicate, turn into, color, or copy link. Tab to indent. Toggle the mode on with .dm-notion-mode.
Every extension you need.
Batteries included: from basic formatting to advanced features.
70+ extensions across 17 packages. All MIT licensed.
View All Packages →Built for production
Architecture decisions that make the difference at scale.
Schema Conflict Detection
Duplicate extension names throw a clear error at startup. No mysterious production bugs from silent overwrites.
Extension Composability
create() → configure() → extend() with this.parent?.() to call the base version. Override any hook while preserving parent behavior.
Fluent Command API
editor.chain().focus().toggleBold().run() chains commands on a shared transaction. editor.can().toggleBold() dry-runs without side effects.
XSS-Hardened URLs
Blocks javascript:, vbscript:, file: protocols on images and links across four layers: parseHTML, renderHTML, commands, and input rules.
A11y Built In
ARIA roles on every node, full keyboard navigation, screen reader tested. Tab through the toolbar, Enter to activate, Esc to close menus. WCAG-friendly by default.
Zero-Jitter Floating UI
Zero JS during scroll events. Floating elements use position: absolute with @floating-ui/dom - the CSS compositor handles the rest.
SSR Ready
Server-side rendering via linkedom. Generate HTML on the server without a browser. Works with Angular Universal, Astro, and any Node.js environment.
Schema Conflict Detection
Duplicate extension names throw a clear error at startup. No mysterious production bugs from silent overwrites.
Extension Composability
create() → configure() → extend() with this.parent?.() to call the base version. Override any hook while preserving parent behavior.
Fluent Command API
editor.chain().focus().toggleBold().run() chains commands on a shared transaction. editor.can().toggleBold() dry-runs without side effects.
XSS-Hardened URLs
Blocks javascript:, vbscript:, file: protocols on images and links across four layers: parseHTML, renderHTML, commands, and input rules.
A11y Built In
ARIA roles on every node, full keyboard navigation, screen reader tested. Tab through the toolbar, Enter to activate, Esc to close menus. WCAG-friendly by default.
Zero-Jitter Floating UI
Zero JS during scroll events. Floating elements use position: absolute with @floating-ui/dom - the CSS compositor handles the rest.
SSR Ready
Server-side rendering via linkedom. Generate HTML on the server without a browser. Works with Angular Universal, Astro, and any Node.js environment.
Get started
in minutes.
A few lines of code is all you need. Pick your framework.
npm install @domternal/core @domternal/theme @domternal/vanilla import { StarterKit, BubbleMenu } from '@domternal/core'; import { DomternalEditor, DomternalToolbar, DomternalBubbleMenu, } from '@domternal/vanilla'; import '@domternal/theme'; const editorEl = document.getElementById('editor')!; const toolbarEl = document.getElementById('toolbar')!; const bubbleEl = document.getElementById('bubble')!; const dm = new DomternalEditor(editorEl, { extensions: [StarterKit, BubbleMenu.configure({ element: bubbleEl })], content: '<p>Hello from Vanilla!</p>', }); new DomternalToolbar(toolbarEl, { editor: dm.editor }); new DomternalBubbleMenu(bubbleEl, { editor: dm.editor });
import { Editor, Document, Text, Paragraph, Bold, Italic, Underline, } from '@domternal/core'; new Editor({ element: document.getElementById('editor')!, extensions: [Document, Text, Paragraph, Bold, Italic, Underline], content: '<p>Hello <strong>Bold</strong>, <em>Italic</em> and <u>Underline</u>!</p>', });
import { Component, signal } from '@angular/core'; import { DomternalEditorComponent, DomternalToolbarComponent, DomternalBubbleMenuComponent, } from '@domternal/angular'; import { Editor, StarterKit, BubbleMenu } from '@domternal/core'; @Component({ selector: 'app-editor', imports: [DomternalEditorComponent, DomternalToolbarComponent, DomternalBubbleMenuComponent], template: ` @if (editor(); as ed) { <domternal-toolbar [editor]="ed" /> } <domternal-editor [extensions]="extensions" [content]="content" (editorCreated)="editor.set($event)" /> @if (editor(); as ed) { <domternal-bubble-menu [editor]="ed" /> } ` }) export class EditorComponent { editor = signal<Editor | null>(null); extensions = [StarterKit, BubbleMenu]; content = '<p>Hello from Angular!</p>'; }
import { Domternal } from '@domternal/react'; import { StarterKit, BubbleMenu } from '@domternal/core'; export default function Editor() { return ( <Domternal extensions={[StarterKit, BubbleMenu]} content="<p>Hello from React!</p>" > <Domternal.Toolbar /> <Domternal.Content /> <Domternal.BubbleMenu /> </Domternal> ); }
<script setup> import { Domternal } from '@domternal/vue'; import { StarterKit, BubbleMenu } from '@domternal/core'; const extensions = [StarterKit, BubbleMenu]; </script> <template> <Domternal :extensions="extensions" content="<p>Hello from Vue!</p>" > <Domternal.Toolbar /> <Domternal.Content /> <Domternal.BubbleMenu /> </Domternal> </template>
Free forever.
No asterisk.
Everything in Open Source, plus:
Start building.
Ship something durable.
Open source. MIT licensed. Free forever.