Extensions
Everything the editor can do comes from extensions. Three kinds, same as Tiptap:
Node— a block or inline node in the document (paragraph,heading,bulletList).Mark— inline formatting applied to text (bold,code,link).Extension— no schema of its own, just commands/shortcuts/rules (undoRedo,starterKit()).
starterKit()
starterKit() is an aggregate: it contributes no schema itself, it just returns the essential set from addExtensions().
import { starterKit } from "@domphy/editor"
import { createEditor } from "@domphy/editor/domphy"
const editor = createEditor({ extensions: [starterKit()] })| Extension | Commands | Shortcuts | Input rules |
|---|---|---|---|
Document | — | — | — |
Text | — | — | — |
Paragraph | setParagraph() | Mod-Alt-0 | — |
Heading | setHeading({ level }), toggleHeading({ level }) | Mod-Alt-1…Mod-Alt-6 | # … ###### |
Bold | setBold(), toggleBold(), unsetBold() | Mod-b | **text**, __text__ |
Italic | setItalic(), toggleItalic(), unsetItalic() | Mod-i | *text*, _text_ |
Strike | setStrike(), toggleStrike(), unsetStrike() | Mod-Shift-s | ~~text~~ |
Underline | setUnderline(), toggleUnderline(), unsetUnderline() | Mod-u | — |
Code | setCode(), toggleCode(), unsetCode() | Mod-e | `text` |
Blockquote | setBlockquote(), toggleBlockquote(), unsetBlockquote() | Mod-Shift-b | > |
BulletList | toggleBulletList() | Mod-Shift-8 | - , + , * |
OrderedList | toggleOrderedList() | Mod-Shift-7 | 1. |
ListItem | — | Enter splits, Tab sinks, Shift-Tab lifts | — |
HardBreak | setHardBreak() | Mod-Enter, Shift-Enter | — |
HorizontalRule | setHorizontalRule() | — | ---, *** |
CodeBlock | setCodeBlock({ language }), toggleCodeBlock({ language }) | Mod-Alt-c | ```lang, ~~~lang |
Link | setLink({ href }), toggleLink({ href }), unsetLink() | — | — |
UndoRedo | undo(), redo() | Mod-z, Shift-Mod-z, Mod-y | — |
TrailingNode | — | — | — |
Mod is Ctrl on Windows and Linux, Cmd on macOS.
Two names are worth knowing because they differ from what you would guess:
Document's schema name is"doc", not"document"— thestarterKit()option key isdocument, but anything addressing the node type (isActive, content expressions) uses"doc".UndoRedoregisters no commands of its own.undo()andredo()are generic engine commands that exist regardless; the extension only carriesdepth/newGroupDelayand the keymap, so dropping it withundoRedo: falseremoves the shortcuts, not the commands.TrailingNodehas no commands and no shortcuts. It keeps an empty paragraph at the end of the document so there is always somewhere to click below a table or code block. Its options arenode(default"paragraph") andnotAfter; the appended node is taggedaddToHistory: false, so undo never stops on it.
Configuring the kit
Every key of the starterKit() options object takes that sub-extension's options, or false to leave it out entirely:
starterKit({
heading: { levels: [1, 2, 3] }, // only H1-H3, so only Mod-Alt-1..3 bind
codeBlock: false, // drop code blocks (and their input rules)
undoRedo: { depth: 50, newGroupDelay: 300 },
link: { HTMLAttributes: { rel: "noopener noreferrer", target: "_blank" } },
})Individual extensions are also exported by name, so you can skip the kit and assemble the set yourself. Document, Text and Paragraph are the minimum a working document needs:
import { Bold, Document, Italic, Paragraph, Text } from "@domphy/editor"
const editor = createEditor({ extensions: [Document, Paragraph, Text, Bold, Italic] })Tables
Tables are not in starterKit() — import the four node extensions yourself. All four are required: a Table holds TableRows, and a row holds cells of either type.
import { starterKit, Table, TableCell, TableHeader, TableRow } from "@domphy/editor"
const editor = createEditor({
extensions: [starterKit(), Table, TableRow, TableHeader, TableCell],
})| Command | Notes |
|---|---|
insertTable({ rows, cols, withHeaderRow }) | Defaults to a 3×3 with a header row. |
deleteTable() | |
addRowAfter() / deleteRow() | Relative to the cell holding the cursor. |
addColumnAfter() / deleteColumn() | Refuses to delete the last remaining column. |
goToNextCell() / goToPreviousCell() |
Tab moves to the next cell and Shift-Tab to the previous one. Tab in the last cell appends a row and moves into it, the same as Tiptap.
Schema: Table is a block containing tableRow+; TableRow contains tableCell+, which is a group both TableCell (td) and TableHeader (th) belong to — so a row accepts either. Cells carry colspan, rowspan and colwidth attributes and contain block+.
This is a subset, not a port of prosemirror-tables. Spans round-trip through HTML but the row and column commands treat the table as a uniform grid and ignore them, so merged cells will drift; there is no cell selection and no column resizing. See Deviations from Tiptap.
configure() vs extend()
configure(options) deep-merges over the result of addOptions() and returns a new instance — the config hooks are untouched. extend(config) replaces hooks; inside a replaced hook, this.parent is the same hook from the extended config, which is how you add to a hook instead of overwriting it:
const HeadingWithBigShortcut = Heading.extend({
addKeyboardShortcuts() {
return {
...this.parent?.(),
"Mod-Alt-9": ({ editor }) => editor.commands.toggleHeading({ level: 1 }),
}
},
})Every config hook is called with this bound to { name, options, storage, editor, parent }.
Lifecycle hooks: onCreate fires once at construction — note the view may not exist yet, since createEditor() mounts later. onMount fires every time the view is mounted into a host element (at construction when element was passed, and on each later editor.mount(host)), so it is the right place to bind DOM listeners to the host — pair it with onDestroy for teardown. onUpdate, onSelectionUpdate, onFocus and onBlur mirror the editor options of the same name.
Writing an extension
A Mark needs a name, how it parses out of HTML, how it renders back, and the commands it contributes:
import { Mark, mergeAttributes } from "@domphy/editor"
const Highlight = Mark.create({
name: "highlight",
parseHTML() {
return [{ tag: "mark" }]
},
renderHTML({ HTMLAttributes }) {
return ["mark", mergeAttributes(HTMLAttributes), 0]
},
addCommands() {
return {
toggleHighlight:
() =>
({ commands }) =>
commands.toggleMark(this.name),
}
},
addKeyboardShortcuts() {
return { "Mod-Shift-h": ({ editor }) => editor.commands.toggleHighlight() }
},
})Commands are curried: the outer function takes the caller's arguments, the inner one takes CommandProps and returns whether it applied. A command must check dispatch before mutating the transaction — can() runs commands with dispatch: undefined precisely so it can ask "would this work?" without changing anything:
addCommands() {
return {
clearHighlight:
() =>
({ tr, state, dispatch }) => {
const { from, to } = state.selection
if (from === to) return false
if (dispatch) tr.removeMark(from, to, this.name)
return true
},
}
}Node extensions add content, group, and addAttributes. Attributes declare a default and are omitted from JSON when every attribute is at its default:
import { Node, mergeAttributes } from "@domphy/editor"
const Callout = Node.create({
name: "callout",
group: "block",
content: "block+",
defining: true,
addAttributes() {
return { tone: { default: "info" } }
},
parseHTML() {
return [{ tag: "aside[data-callout]" }]
},
renderHTML({ HTMLAttributes }) {
return ["aside", mergeAttributes({ "data-callout": "" }, HTMLAttributes), 0]
},
})priority (default 100, higher runs first) decides ordering when two extensions register the same shortcut or parse rule — Paragraph and Link both use 1000.
Next steps
- API Reference — editor options, the generic command set, and the adapter exports
- Toolbar example — wiring these commands to buttons