In an original copy-editing example, a reviewer flags “delivery tomorrow” in a supplier notice. The author inserts a sentence before that phrase. The highlight should move with the phrase; the exported notice should not acquire review-only markup.
Build the highlight from document positions
This plugin works inside an existing ProseMirror application. It requires compatible prosemirror-state and prosemirror-view packages and an editor schema already configured for the document.
import { Plugin, PluginKey } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view';
export const reviewKey = new PluginKey('deliveryReview');
export const deliveryReview = new Plugin({
key: reviewKey,
state: {
init() { return DecorationSet.empty; },
apply(tr, previous) {
const mapped = previous.map(tr.mapping, tr.doc);
const request = tr.getMeta(reviewKey);
if (request?.type === 'clear') return DecorationSet.empty;
if (request?.type === 'highlight') {
const { from, to } = request;
if (from < 0 || to > tr.doc.content.size || from >= to) {
return DecorationSet.empty;
}
return DecorationSet.create(tr.doc, [
Decoration.inline(from, to, { class: 'delivery-question' })
]);
}
return mapped;
}
},
props: {
decorations(state) { return reviewKey.getState(state); }
}
});
Include deliveryReview in the plugins array passed to the application's EditorState.create(). Its key must identify one active plugin in that state. The example replaces the current highlight when a new one is requested; it does not maintain a collection of independent comments.
Provide CSS in the document that contains the editing surface:
.ProseMirror .delivery-question {
background: #fff0b3;
text-decoration: underline dotted #805600;
}
The class is presentation. The DecorationSet supplies the positions that tell the view where to apply it.
Use the current selection, not a text-string offset
With view referring to the initialized editor, a review button can highlight a nonempty text selection:
const { from, to, empty } = view.state.selection;
if (!empty) {
view.dispatch(view.state.tr.setMeta(reviewKey, {
type: 'highlight', from, to
}));
}
For the notice, select exactly “delivery tomorrow” before pressing that button. ProseMirror positions belong to its document tree. They are not interchangeable with an index from textContent.indexOf(), especially across paragraphs and nested nodes. Limit the application's button to the selection types it supports.
When a request includes positions for newly transformed content, those positions must address the transaction's resulting document. This example captures an unchanged current selection and sends a metadata-only transaction, which avoids that conversion step.
Map movement, then decide what deletion means
previous.map(tr.mapping, tr.doc) updates existing decoration positions after edits. Insert a sentence before the selected phrase and inspect whether the highlight still surrounds the intended words. A decoration can be dropped when its associated content is deleted. Mapping preserves position relationships; it does not decide whether a rewritten sentence still makes the same delivery promise.
For multiple review findings, use stable finding IDs in decoration specs and define your add/remove policy. ProseMirror's upload placeholder example demonstrates finding a mapped decoration by ID after the document has changed. Its asynchronous placeholder is a useful position-tracking pattern, while a copy-editing finding still needs its own resolution rules.
Choose a node decoration when styling an entire node and a widget decoration when inserting a view-only DOM element at a position. A node decoration requires boundaries around the node. An inline decoration fits a text range such as this delivery phrase. Decoration and DecorationSet APIs
Keep review presentation separate from the final notice
Clear the finding after the reviewer resolves it:
view.dispatch(view.state.tr.setMeta(reviewKey, { type: 'clear' }));
Export the application's document through its normal schema-aware serializer. Decorations are supplied to the view and do not become document marks merely because the editor displays a highlight. If a finding must survive reopening, separately persist its identity, anchors, and review status with a deliberate restoration strategy.
The Tiptap placeholder guide shows another view-only cue. A placeholder helps an author begin; this highlight follows an existing range through edits. For the supplier notice, finish by reading the exported delivery wording and confirming that the reviewer resolved the promise itself, rather than merely removing its yellow background.