Word-style tracked changes for the web — free, open source, framework-agnostic.
Add "Suggesting mode" to any rich-text field: every edit becomes a suggestion your reviewers can accept or reject, just like Microsoft Word or Google Docs. Built on Tiptap / ProseMirror, with ready-made editors for Vue, React, and plain JavaScript.
Type, delete, bold something, or paste a screenshot, then accept or reject each change from the sidebar. Switch between Alice and Bob to see per-author suggestions. The same demo runs on each adapter, with its source code alongside: Vanilla JS · Vue 3 · React.
Track changes is usually a paid add-on for rich-text editors (CKEditor 5's Track Changes, for example, is a commercial feature). This project gives you the same workflow under the Apache 2.0 license, on top of only the free, MIT-licensed tier of Tiptap/ProseMirror.
- ✍️ Real suggesting mode. Typing, deleting, pasting, formatting (bold, italic, headings, lists, links), images, table rows and columns are all tracked, with the author and a timestamp on every change.
- ✅ Review built in. Accept or reject one change, or all of them. Get a live list of pending suggestions to build your own review sidebar.
- 💾 Store it as plain HTML. Suggestions are ordinary
<ins>/<del>tags withdata-*attributes, not hidden editor state. Save the HTML in aTEXTcolumn, reload it later, and the suggestions are still there. - 🖨️ Prints correctly. Because suggestions are real HTML, they render in print and PDF without the editor (a print stylesheet is included).
- 🧩 Vue, React, or no framework. Three thin adapters over one shared engine, with the same features, the same behaviour, and the same CSS.
- 🔒 Locked-down fields. Only the formatting you enable exists. A plain-text field really is plain text, including keyboard shortcuts and pasted content.
- 🖼️ Images that don't break. Pasted screenshots, image links, and Google Drive / OneDrive / Dropbox links are copied into your storage, so a deleted original never breaks your document.
- 🔢 Word and character limits. Soft limits based on the text as it will read once accepted, with CJK-aware word counting.
- 🪶 Small and headless. No backend, no environment variables, no network calls. You provide the upload function; everything else runs in the browser.
Tiptap and ProseMirror give you an excellent editor, but no review workflow. Every edit changes the document immediately. Here is what this project layers on top:
| Plain Tiptap / ProseMirror | With track-changes | |
|---|---|---|
| Typing and deleting | Applied immediately | Recorded as insertions/deletions with author and timestamp |
| Bold, headings, lists, links | Applied immediately | Recorded as format changes that can be rejected back to the original |
| Images, table rows and columns | Inserted or removed immediately | Each tracked as one reviewable change |
| Review | Not available | accept / reject / acceptAll / rejectAll, plus a live list of pending suggestions |
| Final text | The document is the final text | getCleanHtml() gives the accepted version, while the tracked version stays editable |
| Saving and reloading | StarterKit reads a stored <del> as plain strikethrough |
Suggestions round-trip through HTML or JSON intact |
| Pasted images | Inserted as-is: base64 blobs, or links to someone else's file | Uploaded to your storage; Google Drive / OneDrive / Dropbox links are copied too |
| Character limits | CharacterCount also counts text that is deleted but not yet accepted |
Counts the text as it will read once accepted (CJK-aware) |
| Restricting a field | StarterKit enables every format, shortcut and paste | Only enabled formatting exists in the schema, so a plain-text field stays plain |
| UI | Bring your own | Ready-made editor and toolbar for Vue, React and vanilla JS, plus screen and print CSS |
Pick your framework. Every editor takes the same options.
npm i track-changes-vue track-changes-core track-changes-styles vue \
@tiptap/core@^2 @tiptap/pm@^2 @tiptap/starter-kit@^2 @tiptap/vue-3@^2The
@^2pins matter: these packages support Tiptap 2. A barenpm i @tiptap/coreinstalls Tiptap 3 and fails with a peer-dependency error.
<script setup lang="ts">
import { ref } from 'vue'
import { TrackChangesEditor, createFormattingFeature } from 'track-changes-vue'
import 'track-changes-styles/track-changes.css'
const html = ref('<p>Hello</p>')
</script>
<template>
<TrackChangesEditor
v-model="html"
format="html"
mode="suggesting"
author-id="reviewer-1"
:features="[createFormattingFeature()]"
/>
</template>npm i track-changes-react track-changes-core track-changes-styles react react-dom \
@tiptap/core@^2 @tiptap/pm@^2 @tiptap/starter-kit@^2 @tiptap/react@^2import { useState } from 'react'
import { TrackChangesEditor, createFormattingFeature } from 'track-changes-react'
import 'track-changes-styles/track-changes.css'
const features = [createFormattingFeature()]
export function Field() {
const [html, setHtml] = useState('<p>Hello</p>')
return (
<TrackChangesEditor
value={html}
onChange={(v) => setHtml(v as string)}
format="html"
mode="suggesting"
authorId="reviewer-1"
features={features}
/>
)
}npm i track-changes-vanilla track-changes-core track-changes-styles \
@tiptap/core@^2 @tiptap/pm@^2 @tiptap/starter-kit@^2import { createTrackChangesEditor, createFormattingFeature } from 'track-changes-vanilla'
import 'track-changes-styles/track-changes.css'
const editor = createTrackChangesEditor(document.querySelector('#field')!, {
content: '<p>Hello</p>',
format: 'html',
mode: 'suggesting',
authorId: 'reviewer-1',
features: [createFormattingFeature()],
onChange: (html) => save(html),
})
editor.acceptAll()Each adapter gives you the pending suggestions plus accept/reject actions, so you can build a review panel in a few lines:
const review = useTrackChangesReview(editor) // Vue composable / React hook
review.suggestions.map((s) => (
<li key={s.id}>
{s.authorId} {s.type === 'insertion' ? 'added' : s.type === 'deletion' ? 'removed' : 'formatted'}
“{s.text}”
<button onClick={() => review.accept(s.id)}>Accept</button>
<button onClick={() => review.reject(s.id)}>Reject</button>
</li>
))Need the final text? getCleanHtml() returns the document as it would read
with every suggestion accepted, ready to publish or validate.
| Mode | What happens |
|---|---|
suggesting |
Every edit is recorded as a tracked change. |
editing |
Edits apply directly (for the document owner). |
viewing |
Read-only. Changes can still be accepted or rejected from code. |
Turn on only what each field needs:
| Feature | Adds |
|---|---|
createFormattingFeature() |
Bold, italic, strikethrough, headings, bullet and numbered lists (tracked as format changes) |
createLinkFeature() |
Safe links (http, https, mailto only); setting or removing a link is tracked |
createImageUploadFeature() |
Insert/remove images; pasted and dropped images and image links are copied into your storage |
createTableFeature() |
Tables with a header row; adding or deleting a row or column is tracked as one change |
You can also write your own feature: a feature is just Tiptap extensions plus toolbar buttons.
| Package | Use it for |
|---|---|
track-changes-core |
The engine: suggestion model, accept/reject, HTML codec, and feature plugins. Framework-free. |
track-changes-vue |
<TrackChangesEditor> for Vue 3 |
track-changes-react |
<TrackChangesEditor> for React 18/19 |
track-changes-vanilla |
createTrackChangesEditor() with no framework |
track-changes-styles |
Optional screen and print CSS, themable with CSS variables |
Each package README has the full API. The Vue README covers the shared details: feature options, image storage, and the backend contract for uploads.
Every suggestion lives inside the document as <ins>/<del> tags and
data-suggestion-* attributes carrying { id, authorId, ts }, never in
editor-only state. That one decision is why suggestions survive saving,
copy/paste, printing, and any HTML pipeline, and why they're ready for
real-time collaboration (Yjs) without a redesign.
Read more in the architecture notes.
- Real-time collaboration (Yjs + Hocuspocus)
- Comment threads on suggestions
- Icon toolbar theme
Ideas and pull requests are welcome. Please open an issue first for larger changes.
npm install # install the workspace
npm run build # build core, then the vue/react/vanilla adapters
npm run test # core suite + react/vanilla adapter tests
npm run lint # type-check every package
npm run demo # run the live demo locally (http://localhost:5173)Each package is versioned independently. See its CHANGELOG.md.
Apache License 2.0, Copyright 2026 Dr. Carson Hung. Free for commercial use, with an explicit patent grant. See NOTICE. An independent project, originally built for report revisions at the Technology-Enriched Learning Initiative, The University of Hong Kong.
