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

ActionShortcut
BoldCtrl+B
ItalicCtrl+I
UndoCtrl+Z
RedoCtrl+Y
Wrap in blockquoteCtrl+>

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)