@domphy/editor
@domphy/editor is a rich-text editor whose public API is source-compatible with Tiptap — Editor, Extension/Node/Mark, chainable commands, starterKit() — implemented on an engine of its own. There is no ProseMirror underneath: the document is plain Tiptap-shaped JSON, positions are ProseMirror-style integer token positions, and the view is a contenteditable host driven by beforeinput.
Two layers ship in one package:
@domphy/editor— the framework-agnostic editor. It knows nothing about Domphy elements; you can drive it from any DOM.@domphy/editor/domphy— the Domphy adapter:createEditor, theeditorContent()andbubbleMenu()patches, and theeditorState()reactivity bridge.
Install
npm install @domphy/editorPeer dependencies: @domphy/core and @domphy/theme.
Quick start
createEditor() builds the editor without a DOM host. The editorContent() patch owns the mount: apply it to a div and that element becomes the editing surface, styled with theme tokens.
Typing # , - , > or **bold** applies the matching input rule, and Mod+B / Mod+I / Mod+Z work as expected — those come from starterKit(), not from the adapter.
Reading editor state reactively
The editor exposes stateVersion, a Domphy State<number> bumped once per transaction. Reading it with a listener subscribes that listener to every edit, which is the entire reactivity bridge:
const isBold = (l) => {
editor.stateVersion.get(l)
return editor.isActive("bold")
}
const boldButton = { button: "B", ariaPressed: isBold, onClick: () => editor.chain().focus().toggleBold().run() }editorState(editor) names the two shapes that show up in every toolbar so you do not repeat the stateVersion.get(l) line:
import { editorState } from "@domphy/editor/domphy"
const { isActive, read } = editorState(editor)
isActive("bold") // (l) => boolean
isActive("heading", { level: 2 }) // (l) => boolean — attrs are a subset match
read((editor) => editor.can().undo()) // (l) => boolean
read((editor) => editor.getText()) // (l) => stringComing from Tiptap
The editor half is a straight port — same names, same semantics:
| Tiptap | @domphy/editor |
|---|---|
new Editor({ element, extensions }) | createEditor({ extensions }) + editorContent(editor) patch |
import StarterKit from "@tiptap/starter-kit" | import { starterKit } from "@domphy/editor" |
editor.chain().focus().toggleBold().run() | identical |
editor.can().toggleBold() | identical |
editor.isActive("heading", { level: 2 }) | identical |
<EditorContent editor={editor} /> (React) | { div: null, $: [editorContent(editor)] } |
<BubbleMenu editor={editor}>…</BubbleMenu> | bubbleMenu(editor, { children }) patch |
useEditorState(...) / editor.on("transaction") | editor.stateVersion / editorState(editor) |
What is deliberately not ported, because it is ProseMirror-specific: registerPlugin/unregisterPlugin, node views, mark views, NodePos, paste rules, collaboration, gapcursor, dropcursor, and editorProps passthrough.
One serialization difference: getJSON() omits attrs whose values all equal their defaults, so a level-1 heading serializes as { "type": "heading" } where Tiptap emits "attrs": { "level": 1 }. fromJSON/setContent accept both forms, so Tiptap-produced JSON loads unchanged — only byte-for-byte output comparison differs.
Next steps
- Extensions — the
starterKit()set, its options, and writing your own - API Reference — editor options, commands, and the three adapter exports
- Toolbar example — a full toolbar composed from
@domphy/ui - Bubble Menu example — a menu anchored to the selection