Migrate from v1
Move a browser editor from SuperDoc v1 to the v2 DOCX engine.
If your v1 application mounts SuperDoc from the package root, waits for readiness, and exports through the same instance, most of that integration remains valid. The migration is mainly about selecting the v2 package and removing dependencies on v1 internals.
Migrating with an AI coding agent?
Copy this prompt to have your agent inspect the project against the current migration documentation.
Help me migrate this project from SuperDoc v1 to v2. First, read these sources of truth: /md/editor/migrate-from-v1/overview.md /migration/v1-to-v2.json Inspect the project and report: 1. Removed imports and package subpaths 2. Any direct editor.* access, including commands, state, view, chain(), helpers, comments, presentationEditor, and on() 3. Legacy configuration and collaboration usage 4. Custom UI, extensions, and DOM selectors that require redesign 5. Synchronous Document API reads such as doc.extract(), doc.getMarkdown(), and doc.selection.current(), which the browser resolves as Promises Do not change code yet. Classify each finding using the migration catalog, then propose the smallest safe migration sequence and a verification plan.
1. Install the v2 package
SuperDoc v2 is selected by the installed package version:
pnpm add superdoc@2Keep the package name superdoc. Do not add editorVersion, v2, or v2Integration to the editor configuration. The v2 package always runs the v2 DOCX engine and has no runtime fallback to v1.
2. Keep the supported root integration
These browser integration points remain the same:
| Integration point | V2 path |
|---|---|
| Editor class | SuperDoc from superdoc |
| Styles | superdoc/style.css |
| Document input | document with a File, Blob, or URL |
| Ready lifecycle | onReady or the ready event |
| Mode changes | setDocumentMode() |
| DOCX output | export() |
| Cleanup | destroy() |
A minimal v2 mount still looks familiar:
import { SuperDoc } from 'superdoc';
import 'superdoc/style.css';
const superdoc = new SuperDoc({
selector: '#editor',
document: file,
onReady: ({ superdoc }) => {
superdoc.setDocumentMode('editing');
},
});Call document-dependent methods only after onReady. V2 opens and renders the DOCX progressively, so readiness is the public boundary for starting product interaction.
3. Remove legacy package subpaths
SuperDoc v2 exposes a smaller public package surface:
| V1 import | V2 replacement |
|---|---|
superdoc/types | Not a rename. See Removed in v2 |
superdoc/headless-toolbar | superdoc.ui, the controller the instance owns |
superdoc/headless-toolbar/react | Hooks and providers from superdoc/ui/react |
superdoc/headless-toolbar/vue | Build on superdoc.ui, the framework-agnostic controller |
superdoc/super-editor | Use the SuperDoc instance and its public active-editor facade |
superdoc/converter, superdoc/docx-zipper, or superdoc/file-zipper | Use the supported SuperDoc load and export workflow; there is no direct v2 subpath |
Do not replace a removed public path with an internal package. Packages such as @superdoc/v2-host, @superdoc/headless, and @superdoc/document-api-v2-adapter are implementation details, not customer integration surfaces.
4. Replace direct editor internals
V1 code could reach through the editor to ProseMirror state and commands:
new SuperDoc({
selector: '#editor',
document: file,
onEditorCreate: ({ editor }) => {
const text = editor.state.doc.textContent;
console.log(text);
},
});In v2, read and change document content through the public Document API after the editor is ready:
new SuperDoc({
selector: '#editor',
document: file,
onReady: async ({ superdoc }) => {
const doc = superdoc.activeEditor?.doc;
if (!doc) throw new Error('The active document is unavailable.');
const text = await doc.getText({});
console.log(text);
},
});Do not use editor.state, editor.view, DOM text offsets, or private command objects as the document model. Use the Document API for fresh reads and mutations, and superdoc/ui for reactive custom UI state.
Move UI interactions off the document runtime
DocumentRendererRuntime is part of SuperDoc's rendering path. V1 integrations sometimes retrieved it with
getDocumentRuntimeForDocument() and called navigation methods on it. That lookup is not the V2 readiness boundary. It
can return null when the renderer is not available, which often surfaces as an application error such as
DocumentRuntime is not available.
Read the instance-owned controller from superdoc.ui after onReady instead. The controller groups state and actions by
the feature your interface is showing. It also owns the navigation needed to reveal content on a virtualized page.
Scroll to a tracked change
import type { SuperDoc } from 'superdoc';
export async function scrollToChange(superdoc: SuperDoc, changeId: string): Promise<boolean> {
const result = await superdoc.ui.trackChanges.scrollTo(changeId);
return result.success;
}import type { SuperDoc } from 'superdoc';
export async function scrollToChange(
superdoc: SuperDoc,
documentId: string,
changeId: string,
): Promise<boolean> {
const runtime = superdoc.getDocumentRuntimeForDocument(documentId);
if (!runtime?.scrollToElement) throw new Error('DocumentRuntime is not available.');
return Boolean(await runtime.scrollToElement(changeId));
}Use the narrowest navigation method for the target you already know:
| Target you have | V2 navigation method |
|---|---|
| Tracked-change ID | superdoc.ui.trackChanges.scrollTo(changeId) |
| Comment ID | superdoc.ui.comments.scrollTo(commentId) |
| Content-control ID | superdoc.ui.contentControls.scrollIntoView({ id }) |
| Stable block, comment, or change ID | superdoc.scrollToElement(elementId) |
The feature-specific methods return structured results and keep feature state, such as the active tracked change, in
the same controller. Use superdoc.scrollToElement() when you only have a stable extracted ID and do not know its entity
type. It returns false when SuperDoc cannot route the navigation.
You do not need to import the normal controller from superdoc/ui. The superdoc.ui getter creates it once, and
superdoc.destroy() owns its cleanup. Import createSuperDocUI from superdoc/ui only when you need a separately owned
controller with its own lifecycle. Continue with Custom UI controller setup for
subscriptions and cleanup, or the tracked-changes UI guide for review navigation.
5. Map common editor and ProseMirror internals
These are the mappings most applications need: reading text, resolving a selection, inserting at it, and exporting. Each example includes its imports, inputs, async boundary, and returned value. V2 is selected by default; switching an example switches every example on this page.
For everything else, work from Removed in v2. Each entry names the v1 symbol, what you will observe when it breaks, and the v2 replacement if one exists. Entries link to a canonical API page where one covers the replacement.
Document API
getText
import type { SuperDoc } from 'superdoc';
export async function readText(superdoc: SuperDoc): Promise<string> {
const doc = superdoc.activeEditor?.doc;
if (!doc) throw new Error('The active document is unavailable.');
return doc.getText({});
}import type { SuperDoc } from 'superdoc';
export function readText(superdoc: SuperDoc): string {
return superdoc.activeEditor!.state.doc.textContent;
}selection.current
import type { SuperDoc } from 'superdoc';
export async function currentSelection(superdoc: SuperDoc) {
const doc = superdoc.activeEditor?.doc;
if (!doc) throw new Error('The active document is unavailable.');
return doc.selection.current({ includeText: true });
}import type { SuperDoc } from 'superdoc';
export function currentSelection(superdoc: SuperDoc) {
return superdoc.activeEditor!.state.selection;
}selection.current().selectionTarget
import type { SuperDoc } from 'superdoc';
export async function selectionTarget(superdoc: SuperDoc) {
const doc = superdoc.activeEditor?.doc;
if (!doc) throw new Error('The active document is unavailable.');
const selection = await doc.selection.current({});
return selection.selectionTarget;
}import type { SuperDoc } from 'superdoc';
export function selectionRange(superdoc: SuperDoc) {
const { from, to } = superdoc.activeEditor!.state.selection;
return { from, to };
}insert
import type { SuperDoc } from 'superdoc';
export async function insertText(superdoc: SuperDoc, text: string) {
const doc = superdoc.activeEditor?.doc;
if (!doc) throw new Error('The active document is unavailable.');
// A targetless insert appends at the end of the document. V1 inserted at
// the caret, so a missing target is a lost selection, not a default.
// Failing loudly beats silently putting the text somewhere else.
const selection = await doc.selection.current({});
if (!selection.selectionTarget) {
throw new Error('No selection target. Capture one while the editor has focus, or pass an explicit target.');
}
const result = (await doc.insert({ value: text, target: selection.selectionTarget })) as
| { success: boolean }
| { receipt: { success: boolean } };
const receipt = 'receipt' in result ? result.receipt : result;
if (!receipt.success) throw new Error('Text insertion failed.');
return result;
}import type { SuperDoc } from 'superdoc';
export function insertText(superdoc: SuperDoc, text: string) {
const editor = superdoc.activeEditor!;
return editor.view.dispatch(editor.state.tr.insertText(text));
}Pass a target unless you mean to append. doc.insert({ value }) with no target or ref routes to the end of the
document, so a direct port of tr.insertText(text) appends instead of inserting at the caret. In a browser this
example throws rather than falling back, because losing the selection means the text would land somewhere the user did
not choose. Headless callers with no selection can omit the target deliberately and take the append.
The Editor returns a mutation receipt directly, while the headless SDK returns an operation envelope containing the
receipt. Normalize the two shapes before checking success, as shown above. Headless callers can also use the envelope's
target, resolvedRange, and context for details about the applied mutation. Verify persisted output by saving and
reopening the DOCX when insertion is part of an automated workflow.
Export
getMarkdown
import type { SuperDoc } from 'superdoc';
export async function markdown(superdoc: SuperDoc): Promise<string> {
const doc = superdoc.activeEditor?.doc;
if (!doc) throw new Error('The active document is unavailable.');
return doc.getMarkdown({});
}import type { SuperDoc } from 'superdoc';
export function markdown(superdoc: SuperDoc): string {
return superdoc.activeEditor!.getMarkdown();
}export
import type { SuperDoc } from 'superdoc';
export async function exportDocx(superdoc: SuperDoc) {
return superdoc.export({ exportType: ['docx'], triggerDownload: false });
}import type { SuperDoc } from 'superdoc';
export async function exportDocx(superdoc: SuperDoc) {
return superdoc.export({ exportType: ['docx'], triggerDownload: false });
}6. Migrate changed integration contracts
The following changes are public V2 contracts rather than replacements for ProseMirror internals. Treat the browser Document API as asynchronous, keep document and UI namespaces explicit, and preserve every returned cleanup function.
Collaboration
document.v2Collaboration
import type { Document, V2CollaborationConfig } from 'superdoc';
export function collaborativeDocument(
data: Blob,
id: string,
serverUrl: string,
token: string,
roomMode: 'join' | 'create' = 'join',
): Document {
const v2Collaboration: V2CollaborationConfig = {
providerType: 'hocuspocus', documentId: id, serverUrl, token, roomMode,
};
return {
id, type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', data, v2Collaboration,
};
}import * as Y from 'yjs';
import { HocuspocusProvider } from '@hocuspocus/provider';
const ydoc = new Y.Doc();
const provider = new HocuspocusProvider({ url: serverUrl, name: id, document: ydoc });
const modules = { collaboration: { ydoc, provider } };V2 owns the provider and Y.Doc; do not pass an external pair through modules.collaboration. Use roomMode: 'join'
for an existing room and 'create' only while seeding a missing room. Wait for onCollaborationReady before reading
instance.provider, handle collaboration-v2-room-missing and collaboration-v2-room-already-exists in onException,
and let destroy() release the owned collaboration lifecycle. V2 has no join-or-create mode: retrying with a different
room mode requires a fresh mount.
7. Verify one DOCX round trip
Before migrating advanced UI or automation, verify the smallest complete path:
- Open a representative DOCX in v2.
- Wait for
onReady. - Make one direct or tracked edit.
- Export the document with
SuperDoc.export(). - Reopen the result in Microsoft Word or another DOCX reader.
- Confirm the edited content, formatting, comments, and tracked changes still match the intended document state.
Keep v1 and v2 on separate branches or deployments while comparing the same input documents. A package upgrade should remain easy to reverse until the documents that matter to your product complete this round trip.
Once that round trip holds, migrate whatever else your application uses, working from Removed in v2 and the canonical API pages it links to.
Next, follow the Editor quickstart for a complete v2 mount, or learn the Document API mental model before migrating programmatic edits.