ProseMirror
The foundational toolkit for building rich text editors. Schema-defined documents, immutable state, and functional transactions.
Enhanced Demo
This demo includes a custom "callout" node type defined in the schema, plus a live transaction inspector. Edit the content and watch the transaction log.
Custom Schema
ProseMirror's core idea: the document structure is defined by a schema. This demo extends the basic schema with a custom "callout" node:
typescript
import { Schema } from 'prosemirror-model';
// Custom "callout" node type
const mySchema = new Schema({
nodes: {
doc: { content: 'block+' },
paragraph: {
content: 'inline*', group: 'block',
parseDOM: [{ tag: 'p' }],
toDOM() { return ['p', 0]; }
},
callout: {
content: 'block+', // Can contain paragraphs, lists, etc.
group: 'block',
attrs: { type: { default: 'info' } }, // info, warning, error
parseDOM: [{ tag: 'div.callout',
getAttrs(dom) { return { type: dom.dataset.type }; }
}],
toDOM(node) {
return ['div', {
class: `callout callout-${node.attrs.type}`,
'data-type': node.attrs.type
}, 0];
}
},
heading: {
attrs: { level: { default: 1 } },
content: 'inline*', group: 'block',
parseDOM: [1,2,3,4].map(level => ({
tag: `h${level}`, attrs: { level }
})),
toDOM(node) { return [`h${node.attrs.level}`, 0]; }
},
text: { group: 'inline' }
},
marks: {
bold: {
parseDOM: [{ tag: 'strong' }],
toDOM() { return ['strong', 0]; }
},
italic: {
parseDOM: [{ tag: 'em' }],
toDOM() { return ['em', 0]; }
}
}
});Transaction System
All changes go through immutable transactions — each is a sequence of reversible steps:
typescript
// Transactions are immutable state transformations
view.dispatch(
view.state.tr
.insertText('Hello') // Insert text
.addMark(0, 5, bold) // Add bold mark
.setNodeMarkup(pos, null, { level: 2 }) // Change heading level
);
// Inspect transaction
dispatchTransaction(tr) {
console.log('Steps:', tr.steps.length);
console.log('Doc changed:', tr.docChanged);
console.log('Selection changed:', tr.selectionSet);
// Each step is a reversible operation
tr.steps.forEach(step => {
console.log(step.toJSON());
});
const newState = view.state.apply(tr);
view.updateState(newState);
}Architecture
prosemirror-view DOM rendering, event handling, NodeViews
prosemirror-state Immutable state, transactions, plugins
prosemirror-transform Document transformations (steps)
prosemirror-model Schema, nodes, marks, document structure
Keyboard Shortcuts
| Action | Shortcut |
|---|---|
| Bold | Ctrl+B |
| Italic | Ctrl+I |
| Undo | Ctrl+Z |
| Redo | Ctrl+Y |
| Wrap in blockquote | Ctrl+> |
Strengths
- Maximum control over editor behavior
- Best architecture of any editor
- Proven at scale (NYT, Atlassian, Confluence)
- Built-in collaboration via OT
- Framework-agnostic
- Custom NodeViews for complex rendering
Weaknesses
- Steep learning curve
- No UI included — build everything yourself
- Significant effort to make a usable editor
- Not markdown-native (HTML/JSON model)