Skip to content
Domphy

@domphy/editor

@domphy/editor is a rich-text editor whose public API is source-compatible with TiptapEditor, 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, the editorContent() and bubbleMenu() patches, and the editorState() reactivity bridge.

Install

npm install @domphy/editor

Peer 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) => string

Coming 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