# Overview > Route to the right surface for editing DOCX files from code, a shell, or an agent. SuperDoc changes DOCX files without an editor UI. Some of that work is deterministic: a script knows the operation before it runs. Some of it is model-driven: an agent decides which operation to run from an instruction. Both use the same document engine and the same Document API contract. Headless describes how these workflows run. It does not describe what you are trying to build, so start from the goal instead. ## Start from your goal [#start-from-your-goal] | Goal | Start here | | ------------------------------------------- | ------------------------------------------------------------------ | | Let a coding agent edit DOCX files | [Build an agent](/agents/build/build-an-agent) | | Add document tools to your product's agent | [Build an agent](/agents/build/build-an-agent) | | Run a known operation from application code | [Node.js SDK](/agents/automation/node-sdk) | | Run a known operation from Python | [Python SDK](/agents/automation/python-sdk) | | Run a known operation from a shell or CI | [CLI](/agents/automation/cli) | | Prepare review-aware HTML/Markdown for RAG | [Output projections](/document-api/output-projections) | | Keep a person in the approval loop | [Review tracked changes](/agents/workflows/review-tracked-changes) | | Understand the operation contract itself | [Document API mental model](/document-api/mental-model) | ## The workflow underneath [#the-workflow-underneath] Every headless workflow follows the same sequence, whether a script or a model chooses the operations: ```text open → inspect state → mutate → check receipt → save → close ``` A model-driven workflow adds one step in front of it: ```text instruction → model selects a tool → SuperDoc runs the operation → receipt ``` The split matters. The model decides what to do. SuperDoc runs the operation deterministically and reports a structured result. A model that picks the wrong operation produces a valid receipt for the wrong edit, so treat model mistakes and operation failures as separate problems. ## Which package to install [#which-package-to-install] | Surface | Use it when | Package | | ----------- | --------------------------------------------------- | --------------- | | Node.js SDK | Node.js owns the workflow, errors, and output files | `@superdoc/sdk` | | Python SDK | Python owns the workflow, errors, and output files | `superdoc-sdk` | | CLI | A shell script or CI job runs the workflow | `@superdoc/cli` | The Node.js SDK also provides the agent toolkit, so a Node.js product that embeds its own agent needs only `@superdoc/sdk`. Do not import `@superdoc/headless` or `@superdoc/document-api-v2-adapter`. Those are implementation details rather than integration surfaces. ## What these surfaces share with the Editor [#what-these-surfaces-share-with-the-editor] * Queries return explicit matches, targets, handles, and revisions. * Mutations return receipts or structured failures. * Direct and tracked changes use the same operation contract. * Comments, lists, tables, images, and sections are reached through Document API operations. * DOCX remains the input and output format. Capability availability varies by runtime and document state. Check `doc.capabilities()` before relying on an optional operation. See [How SuperDoc works](/resources/how-superdoc-works) for the engine path and surface ownership map. ## What they do not provide [#what-they-do-not-provide] These hosts never mount the Editor UI, so they have no toolbar, document canvas, viewport, browser selection, or pointer editing. There is no visual surface for accepting or rejecting a suggestion. When a person must decide, write a separate DOCX and open it in the Editor or Microsoft Word. The [tracked-change review workflow](/agents/workflows/review-tracked-changes) shows that handoff end to end. ## Before you ship [#before-you-ship] Your application owns the file and process boundary around the engine. Validate input paths, write results to a separate output until verification succeeds, bound retries, attribute changes to an explicit identity, and close sessions on success and failure alike. [Safety](/agents/operate/safety) covers those responsibilities in full, and matters most once a model is choosing the operations. --- # Store application data in DOCX > Choose the DOCX structure that matches how your application stores and presents data. Choose a document structure by what the data means and how other DOCX tools should understand it. Do not recreate the ProseMirror node or mark that previously carried it. | What you need | Use | Stored as | How it appears | | ----------------------------------------------------- | --------------------- | ----------------------------------------------- | --------------------- | | A Word-compatible citation with a bibliography source | `doc.citations` | A citation field and source record | From the field result | | A calculated or cached Word field | `doc.fields` | An OOXML field instruction and result | From the field result | | A structured region that people can edit | `doc.contentControls` | A content control (`w:sdt`) | Yes | | Application JSON attached to a text range | `doc.metadata` | A hidden inline content control plus Custom XML | Not visible by itself | | Application data that is not attached to text | `doc.customXml` | A Custom XML part | Not visible | Use the public API rather than editing these XML elements directly. SuperDoc keeps the related package parts and relationships consistent when the document changes. ## Anchored application data [#anchored-application-data] Use anchored metadata for an application-owned record such as a review finding, external reference, or backend identifier. The caller chooses the record ID and namespace: ```ts // A user action, not module scope: the guards below return early, and `return` // is only legal inside a function. async function attachFinding() { const doc = superdoc.activeEditor?.doc; // `capture()` reports readiness; `current()` does not. A worker-backed read is // `pending` or `stale` while it settles and still carries the previous range, // so writing from it anchors the record to the user's earlier selection. // // Read `target`, not `selectionTarget`: `target` is always a `TextTarget` // (one segment per paragraph the selection touches), which is what lets a // selection spanning multiple paragraphs attach as one record. // `selectionTarget` collapses a multi-paragraph selection to its first/last // endpoints and cannot express that. const capture = superdoc.ui.selection.capture(); const target = capture?.status === 'ready' ? capture.target : null; // Anchoring is one or more non-empty text ranges — one segment per // paragraph — in the main document **body**. Check every part of that // before writing, rather than handling rejections: // // - Every segment must be non-empty. // - `story` is per-target: a non-body target (header, footer, footnote, // endnote, textbox) is rejected outright, since the adapter resolves // paragraphs against the main document part only. // - A selection crossing deleted tracked text reports `coordinateSpace: // 'tracked'`, and the anchor rewrite searches visible `w:t` runs only, so // the same offsets address different characters. // // These guards are necessary but not sufficient. A selection crossing a // table-cell or section boundary still throws, and nothing in the public // selection shape reports that ahead of time, so the catch below is the // only place that case can be handled. if (!doc || !target) return; if (target.coordinateSpace === 'tracked') return; if (target.segments.length === 0) return; if (target.segments.some((segment) => segment.range.start === segment.range.end)) return; if (target.story !== undefined && target.story.storyType !== 'body') return; // `attach()` throws for several ordinary cases instead of returning an // unsuccessful receipt: when the id already exists (including the second time // this action runs), when segments are not contiguous paragraphs (or cross a // table-cell/section boundary), when a segment overlaps a content control, // a hyperlink, or a simple field (`w:fldSimple`, code and cached result // alike — a complex field's cached result text, outside any wrapper, is // the one field-adjacent case that IS attachable), and when a paragraph is // not in the main document part. // // Use try/catch rather than `.catch()`. This facade is typed `MaybePromise`, so // on a synchronous host `attach()` returns the receipt itself and has no // `.catch` method, and a synchronous validation throw happens before any // promise exists to reject. try { const receipt = await doc.metadata.attach({ id: 'finding-42', namespace: 'urn:example:review', target, payload: { sourceId: 'source-7', justification: 'Verify this statement' }, }); if (!receipt.success) reportToUser(receipt.failure.message); } catch (error) { reportToUser(error instanceof Error ? error.message : String(error)); } } ``` Metadata persists with the DOCX but does not add visible styling. Add a render-only extension visual when people need to see the anchored range. The anchor is painted as a content control — one per paragraph it spans, all sharing the same tag when the anchor covers multiple paragraphs. A viewport hit carries the metadata ID in `tag`, while `id` identifies the underlying content control. A record with that ID existing is not proof the click landed on its anchor: nothing stops an ordinary content control from carrying a tag that matches a record anchored elsewhere, including elsewhere in the same paragraph. Comparing the clicked control's full range with where the record resolves rules out that case, though not every case, as the note after the example explains: ```ts import type { SelectionTarget, TextTarget } from 'superdoc/ui'; // Both endpoints must be `text` to compare positions at all: a `nodeEdge` // endpoint carries a node reference rather than a block and offset. // // Compare the whole story, not just its `storyType`. Two different headers are // both `headerFooterPart`, and block ids are only unique within a story, so // comparing the type alone lets an anchor in one header match a control in // another. The Document API has an internal canonical key for this; until it is // public, discriminate on the fields that identify each variant. const storyKey = (story: SelectionTarget['story']) => { if (!story) return 'body'; switch (story.storyType) { case 'headerFooterPart': return `headerFooterPart:${story.refId}`; case 'headerFooterSlot': return `headerFooterSlot:${story.section.sectionId}:${story.headerFooterKind}:${story.variant}`; case 'footnote': case 'endnote': return `${story.storyType}:${story.noteId}`; case 'textbox': return `textbox:${story.textboxId}`; default: return 'body'; } }; // `anchor.target` is a plain `SelectionTarget` for a single-paragraph anchor // (unchanged since v1), or a `TextTarget` for one spanning multiple // paragraphs — one control per segment, all sharing the clicked tag. A single // clicked control is always confined to one paragraph, so match its range // against ANY ONE of the anchor's segments rather than the whole anchor's // start-to-end span. const controlMatchesAnchor = (anchor: SelectionTarget | TextTarget, control: SelectionTarget) => { if (control.start.kind !== 'text' || control.end.kind !== 'text') return false; if (control.start.blockId !== control.end.blockId) return false; if (storyKey(anchor.story) !== storyKey(control.story)) return false; const controlBlockId = control.start.blockId; const controlStart = control.start.offset; const controlEnd = control.end.offset; if (anchor.kind === 'text') { return anchor.segments.some( (segment) => segment.blockId === controlBlockId && segment.range.start === controlStart && segment.range.end === controlEnd, ); } if (anchor.start.kind !== 'text' || anchor.end.kind !== 'text') return false; return ( anchor.start.blockId === controlBlockId && anchor.start.offset === controlStart && anchor.end.offset === controlEnd ); }; editorShell.addEventListener('pointerdown', async (event) => { const doc = superdoc.activeEditor?.doc; if (!doc) return; const context = superdoc.ui.viewport.contextAt({ x: event.clientX, y: event.clientY }); // Hits are innermost-first, and a metadata anchor can contain an ordinary // nested control. Test every content-control hit rather than the innermost, // or a nested control shadows the anchor that encloses it. // // A non-body hit cannot be filtered out here, and it cannot be detected // afterwards either. `context.position` is always `null` in v2 and // `hit.story` is populated only for tracked-change hits, so nothing reports // the story of a content control. Both lookups below read the main document // part, and painted ids are unique only within that part, so a header, // footer, note, or textbox control that reuses a body anchor's id and tag // resolves the body record and passes every check below. // // The example opens the record because a colliding non-body control is // unlikely in documents this application produced. That is a judgement about // your corpus, not a guarantee from the API. If your documents come from // elsewhere, treat a match as unverified and confirm through your own // backend before acting on it. for (const hit of context.entities.filter((entity) => entity.type === 'contentControl')) { if (!hit.tag) continue; // A metadata anchor is a hidden **inline** content control, so `kind` below // must say `inline`. Read it from the hit rather than hardcoding it: `scope` // is absent when the painted control carries no scope attribute, and a // block control cannot be an anchor, so both cases skip. if (hit.scope !== 'inline') continue; // Read the control through the Document API rather than the UI catalog. // `ui.contentControls.list()` schedules its read and returns whatever is // cached, so an early click sees an empty or stale array; the awaited // Document API call always answers about the current document. // // Compare the whole range, not just the block: a colliding tag can sit in // the same paragraph as the real anchor. // // `get()` throws rather than returning null for an id it cannot find, and // an uncaught throw here would surface as an unhandled rejection from this // listener. Treat a failed lookup as "not this hit" and keep scanning. let control: Awaited> | null = null; let anchor: Awaited> | null = null; try { [control, anchor] = await Promise.all([ doc.contentControls.get({ target: { kind: 'inline', nodeType: 'sdt', nodeId: hit.id } }), doc.metadata.resolve({ id: hit.tag }), ]); } catch { continue; } if (!anchor || !control?.selectionTarget) continue; if (!controlMatchesAnchor(anchor.target, control.selectionTarget)) continue; const record = await doc.metadata.get({ id: hit.tag }); if (record) openApplicationRecord(record); return; } }); ``` > **The range comparison narrows the risk without closing it (warning)** > > The adapter treats the public `w:alias` value `Anchored metadata` as proof on its own that a control is an anchor, > without checking the rest of the anchor shape. That alias is the **Title** field in Word's content-control properties > dialog, so any author or any other tool can set it. Same-tag controls are grouped by document position into the > anchor's segments, so a control carrying that alias and a colliding tag near the real anchor can be absorbed as an > extra segment (or, if it breaks the contiguous run, excluded and resolved as a separate, wrong match) — either way, > the comparison above still succeeds, because both sides describe the same wrong control. Treat an anchored record as a > hint from document content rather than as an authenticated identity, and keep anything security-relevant in your own > backend, keyed by the record ID. Anchored metadata requires one or more non-empty text ranges, in visible coordinates, anchored in the main document **body**. A single-paragraph target is a `SelectionTarget`, unchanged since v1. A target spanning multiple paragraphs is a `TextTarget` with one segment per paragraph: the segments must be contiguous paragraphs in document order, and none of the boundary they cross may be a table cell, a block-level content control, or a section — a target that crosses any of those is rejected. Within a paragraph, a segment may freely cross bold, italic, colour, or character-style boundaries — the anchor preserves each run's own formatting rather than requiring the whole segment to sit in one run — but a segment that overlaps an existing content control, a hyperlink, or a `w:fldSimple` field is rejected, since anchoring inside one of those would either nest content controls or land in text that isn't real anchorable content. A `w:fldSimple` is rejected in full, code and cached result alike; a complex field (`w:fldChar`/`w:instrText`) is different — only its instruction-code runs are rejected, and its cached result text is ordinary anchorable content, same as any other run. The adapter resolves every paragraph against the main document part, so a header, footer, or note paragraph is not found, and the anchor rewrite searches visible `w:t` runs, so a selection crossing deleted tracked text addresses different characters than its offsets suggest. Resolve the record again with `doc.metadata.resolve({ id })` after document edits instead of keeping an old range — it returns the same `SelectionTarget | TextTarget` union, narrowing to `TextTarget` only when the anchor spans more than one paragraph. The read path carries the same body-only restriction, and gives you nothing to check it with. `contentControls.get()` and `metadata.resolve()` both read the main document part, while painted content-control ids are unique only within that part. A control in a header, footer, note, or textbox that reuses a body anchor's id and tag therefore resolves the body record, and every comparison the example makes succeeds against it. Nothing lets you tell the two apart: `context.position` is always `null` in this release, and `hit.story` is populated only for tracked-change hits, so neither reports the story of a content control. Anything in a paragraph that is not literal text shifts the two coordinate systems apart. A selection counts a tab, a line break, and an inline image as caret positions, while the anchor rewrite accumulates offsets from `w:t` contents alone. Any of them before or inside the selection makes the stored anchor land later in the paragraph than the user selected, and a long enough run of following text absorbs the difference so the write succeeds instead of failing. Tabs make this ordinary rather than exotic: a hanging-indent list item begins with one. There is no public option to reconcile the two coordinate systems today, so prefer selections in paragraphs with no tabs, breaks, or inline objects before the range, and verify anchors in exported files when a paragraph has them. ## Citation or application record? [#citation-or-application-record] Use `doc.citations` when Word and other DOCX tools should recognize the value as a citation backed by a bibliography source. Use `doc.metadata` when the meaning belongs to your application and the document only needs to preserve its ID, payload, and text anchor. These models are not interchangeable. Check the generated operation reference for the current insertion and presentation capabilities before migrating a citation workflow. HTML and Markdown projection maps serve a different kind of citation: they map a UTF-16 range in serialized output back to a public tracked-coordinate `TextTarget`. They do not create a Word bibliography citation. Keep the map with its `evaluatedRevision`, and [reproject after a mutation](/document-api/output-projections#map-output-back-to-document-content) before using the target for a comment, link, or application record. --- # Create and resolve comment threads > Anchor a DOCX comment to document content, add a reply, and resolve the thread. Comments are document content. Use the Document API when code needs to create, inspect, update, or delete a thread without driving the built-in comments interface. This guide creates one anchored thread, adds a reply, and resolves it. The operations work against an open document in the Editor and through supported headless clients. ## Run the complete workflow [#run-the-complete-workflow] Copy the [tracked-changes fixture](/fixtures/tracked-changes.docx) to your app's public directory as `contract.docx`, then run this browser example: ```ts import { SuperDoc } from 'superdoc'; import 'superdoc/style.css'; const superdoc = new SuperDoc({ selector: '#editor', document: '/contract.docx', onReady: async ({ superdoc }) => { const doc = superdoc.activeEditor?.doc; if (!doc) throw new Error('The active document is unavailable.'); const match = await doc.query.match({ select: { type: 'text', pattern: 'Confidential Information', }, require: 'exactlyOne', }); const clause = match.items[0]; if (!clause || clause.matchKind !== 'text') { throw new Error('The clause was not found.'); } const createReceipt = await doc.comments.create( { target: clause.target, text: 'Confirm that this definition matches the current policy.', }, { expectedRevision: match.evaluatedRevision }, ); if (!createReceipt.success) { throw new Error(`Comment creation failed: ${createReceipt.failure.message}`); } const afterCreate = await doc.comments.list({ includeResolved: true }); const replyReceipt = await doc.comments.create( { parentCommentId: createReceipt.id, text: 'Confirmed against the policy dated July 2026.', }, { expectedRevision: afterCreate.evaluatedRevision }, ); if (!replyReceipt.success) { throw new Error(`Reply failed: ${replyReceipt.failure.message}`); } const afterReply = await doc.comments.list({ includeResolved: true }); const resolveReceipt = await doc.comments.patch( { commentId: createReceipt.id, status: 'resolved', }, { expectedRevision: afterReply.evaluatedRevision }, ); if (!resolveReceipt.success) { throw new Error(`Resolve failed: ${resolveReceipt.failure.message}`); } console.log('Resolved comment:', createReceipt.id); }, }); window.addEventListener('beforeunload', () => superdoc.destroy()); ``` The example deliberately re-lists comments between mutations. Each mutation advances the document revision, so the next operation uses a current `expectedRevision`. ## Anchor the root comment [#anchor-the-root-comment] A root comment needs text and a document target. A text query returns a `SelectionTarget` that can be passed directly to `comments.create()`. Do not derive the target from the rendered DOM or from `highlightRange`. DOM positions belong to layout. `highlightRange` describes the displayed snippet. The query target addresses document content. On success, the creation receipt includes the new thread ID. Keep that ID for replies and later lifecycle changes. ## Add replies without another target [#add-replies-without-another-target] A reply belongs to an existing thread. Pass `parentCommentId` and text. Do not pass the root target again. The reply is another document mutation. Inspect its receipt before resolving the thread or saving the file. ## Resolve or reopen the thread [#resolve-or-reopen-the-thread] `comments.patch()` changes exactly one comment field per call. Set `status: 'resolved'` to resolve the root thread. Set `status: 'active'` in a later call to reopen it. List with `includeResolved: true` when the application needs to show both states. A resolved thread remains in the DOCX unless it is explicitly deleted. Use [comments in the built-in UI](/editor/built-in-ui/comments) for the standard human workflow. Use [a custom comments UI](/editor/custom-ui/comments) when your application owns the thread list and navigation. ## Follow comments through HTML or Markdown [#follow-comments-through-html-or-markdown] Detailed HTML and Markdown projections include one annotation record per in-scope comment. A discontinuous comment keeps one public ID and can have several source or output ranges. The record is `partiallyEmitted` when only some segments survive the chosen scope. Review mode also affects anchors. A comment attached only to inserted content is `omitted` in the original view; one attached only to deleted content is `omitted` in the final view. An omitted record has no output range and reports why. Do not search nearby serialized text and invent a replacement anchor. Use the record's tracked `sourceTarget` when it is available, keep it with the projection's `evaluatedRevision`, and reproject after a mutation. See [Project HTML and Markdown](/document-api/output-projections) for the source-map and citation round-trip contract. --- # Document API mental model > Query a document, identify a target, apply a mutation, and inspect its receipt. The Document API is the operation contract for reading and changing a SuperDoc document. Browser and headless hosts expose the same operation names and data shapes. Most workflows follow four steps: 1. Query the document. 2. Keep the returned address or target. 3. Apply a mutation to that target. 4. Inspect the mutation receipt. > **Diagram:** A document operation moves from a query to a stable target, then through a mutation that returns a receipt. ## 1. Query [#1-query] Use `doc.query.match(...)` to find content by meaning or structure. A query returns document-native references for the next step. Follow [Query document content](/document-api/query-content) for a complete browser example and the result fields to keep. ### Browser ```ts const match = await editor.doc.query.match({ select: { type: 'text', pattern: 'termination' }, require: 'first', }); const result = match.items[0]; if (!result || result.matchKind !== 'text') { throw new Error('No matching text found.'); } const operation = await editor.doc.replace({ target: result.target, text: 'cancellation', }, { changeMode: 'tracked' }); ``` ### Headless ```ts const match = await doc.query.match({ select: { type: 'text', pattern: 'termination' }, require: 'first', }); const result = match.items[0]; if (!result || result.matchKind !== 'text') { throw new Error('No matching text found.'); } const operation = await doc.replace({ target: result.target, text: 'cancellation', changeMode: 'tracked', }); ``` Do not derive mutation locations from rendered DOM nodes or copied text offsets. The DOM can change when layout changes. A Document API result belongs to the document model. ## 2. Address or target [#2-address-or-target] An address identifies a document location or object. A target describes the content a mutation should affect. Some query results provide a mutation-ready target. Other workflows resolve an address into the target required by the operation. Keep the target returned for the current document revision. If the document changes first, query again instead of assuming that an old target still points to the same content. ## 3. Mutate [#3-mutate] Pass the target to the operation that makes the change. The target makes the scope explicit. It also lets each host apply the same operation contract without inspecting its UI state. Tracked-change review follows this shape. `doc.trackChanges.list()` discovers changes. `doc.trackChanges.get()` reads one change. `doc.trackChanges.decide({ decision, target })` applies a decision to an explicit target. ## 4. Receipt [#4-receipt] A successful mutation returns a receipt that records what the engine applied. Use it to confirm the result and continue from the resolved effects. Treat the receipt as part of the contract. Do not infer success from a repaint, a changed file size, or the absence of an error. **Receipt `replace`**: replacement recorded in tracked mode ## Runtime shape [#runtime-shape] Browser calls are Promise-shaped. Other clients expose their own synchronous or asynchronous form. The operation names, inputs, outputs, targets, errors, and receipt meaning stay the same. This separation is deliberate. The host decides how a call runs. The Document API decides what the call means. ## Use the contract in a workflow [#use-the-contract-in-a-workflow] - [Mount an editor](/editor/quickstart): Open a DOCX, make a tracked edit, and export the result in a browser application. - [Run a headless operation](/agents/automation/node-sdk): Query a DOCX, accept its tracked changes, and save a separate output from Node.js. - [Connect code to human review](/agents/workflows/review-tracked-changes): Create a tracked replacement, review the exact output in the editor, and export the decision. ## Reference [#reference] The [generated Document API reference](/document-api/reference) is derived from the canonical public contract and lists the current operations, inputs, outputs, and failure modes. --- # Preview and apply mutation plans > Validate several document edits, then apply them together against one document revision. Use a mutation plan when several edits belong to one logical change. A plan resolves every target, previews the combined work without changing the document, and applies valid steps as one atomic transaction. For one independent replacement or deletion, prefer the direct operations in [Replace and delete content](/document-api/replace-delete-content). > **Diagram:** Two query references become one atomic plan that is previewed before all steps apply together. ## Run a complete browser example [#run-a-complete-browser-example] Create a browser app with an `#editor` mount element and an explicit container height, as shown in the [Editor quickstart](/editor/quickstart). This example expects the [tracked-changes fixture](/fixtures/tracked-changes.docx) at `/contract.docx`. Its source lives in the docs app and is typechecked against the public v2 browser API: ```ts import { SuperDoc } from 'superdoc'; import 'superdoc/style.css'; const superdoc = new SuperDoc({ selector: '#editor', document: '/contract.docx', onReady: async ({ superdoc }) => { const doc = superdoc.activeEditor?.doc; if (!doc) throw new Error('The active document is unavailable.'); const [companyResult, liabilityResult] = await Promise.all([ doc.query.match({ select: { type: 'text', pattern: 'Amazing' }, require: 'exactlyOne', }), doc.query.match({ select: { type: 'text', pattern: '$500,000' }, require: 'exactlyOne', }), ]); const company = companyResult.items[0]; const liability = liabilityResult.items[0]; if (!company || company.matchKind !== 'text' || !liability || liability.matchKind !== 'text') { throw new Error('The expected contract text was not found.'); } if (companyResult.evaluatedRevision !== liabilityResult.evaluatedRevision) { throw new Error('The document changed while the plan targets were being collected.'); } const plan = { expectedRevision: companyResult.evaluatedRevision, atomic: true as const, changeMode: 'tracked' as const, steps: [ { id: 'rename-company', op: 'text.rewrite' as const, where: { by: 'ref' as const, ref: company.handle.ref }, args: { replacement: { text: 'Northstar' } }, }, { id: 'lower-liability-cap', op: 'text.rewrite' as const, where: { by: 'ref' as const, ref: liability.handle.ref }, args: { replacement: { text: '$250,000' } }, }, ], }; const preview = await doc.mutations.preview(plan); if (!preview.valid) { throw new Error(preview.failures?.map((failure) => failure.message).join('; ') ?? 'Plan preview failed.'); } const receipt = await doc.mutations.apply(plan); console.log('Applied steps:', receipt.steps); }, }); window.addEventListener('beforeunload', () => { superdoc.destroy(); }); ``` The example queries both targets before applying any change. It also confirms that both matches came from the same document revision before building the plan. ## Build the plan from references [#build-the-plan-from-references] Use `item.handle.ref` for plan steps. Each ref points to content resolved by `query.match()`, and `expectedRevision` binds the plan to the document state that produced those refs. Every plan requires: * `atomic: true` * `changeMode: 'direct'` or `'tracked'` * A stable, unique `id` for every step * A supported step `op` * A `where` target and operation-specific `args` Check `doc.capabilities().planEngine.supportedStepOps` before constructing plans dynamically. The generated reference remains authoritative for each step shape. ## Preview without changing the document [#preview-without-changing-the-document] `doc.mutations.preview(plan)` resolves targets and validates every step without applying document changes. Read: * `valid` before calling `apply()` * `failures` for the step ID, phase, code, and message * `steps` for the targets each step resolved * `evaluatedRevision` for the state used during preview A valid preview is still a snapshot. Another writer can change the document before apply, so keep `expectedRevision` on the plan. ## Apply atomically [#apply-atomically] `doc.mutations.apply(plan)` commits every step together. If compilation, target resolution, an assertion, or revision validation fails, the plan does not partially apply successful steps. The returned plan receipt includes the before/after revision, a result for every step, any tracked-change addresses, and timing metadata. Use step results for verification and diagnostics, not for inventing performance claims. > **Verification target (success)** > > The preview should report `valid: true`. Apply should return two changed steps, and the Editor should show `Northstar > Corp` plus a `$250,000` liability cap as tracked changes. The generated [`mutations.preview` reference](/document-api/reference/mutations/preview) and [`mutations.apply` reference](/document-api/reference/mutations/apply) list supported steps, limits, outputs, and failure codes. --- # Project HTML and Markdown > Choose a review view, scope document content, inspect fidelity, and map serialized output back to document targets. Use `projectHtml()` or `projectMarkdown()` when an application needs more than a display string. These asynchronous reads return serialized content together with the document revision, diagnostics, block ranges, annotation status, and an optional output-to-source map. Use `doc.capabilities.check({ operation: 'projectHtml' | 'projectMarkdown', input })` when a workflow needs the same structured support envelope used for rich writes. The check runs the detailed projector and returns its complete output in `projection`; it does not ask you to repeat a revision-sensitive read. Direct detailed projections and support-check projections both include the common fidelity `outcome` alongside `status`, `lossy`, and all diagnostics. Use [`getHtml()`](/document-api/reference/get-html) or [`getMarkdown()`](/document-api/reference/get-markdown) when only the compact string is needed. The compact methods use the same V2 projection rules but do not expose diagnostics or provenance metadata. Migrating is additive: ```ts const html = doc.getHtml({}); const projection = await doc.projectHtml({ reviewMode: 'final', includeSourceMap: true, }); const detailedHtml = projection.content; ``` In the browser facade, await both forms because browser reads may cross the worker boundary. The headless `DocumentApi` keeps the compact getters synchronous for compatibility; the detailed methods are asynchronous in every host. ## Choose the review view [#choose-the-review-view] `reviewMode` controls which side of open tracked changes becomes content: | Mode | Content | | ---------- | --------------------------------------------------------------------------------------------------- | | `final` | Includes insertions and destinations; excludes deletions and move sources | | `original` | Includes deletions and move sources; excludes insertions and destinations | | `redline` | Includes representable before and after sides with semantic change carriers and annotation metadata | The rule applies to inline text and structural revisions such as paragraphs, list items, table rows and cells, and section boundaries. Formatting-only changes emit one content copy in redline with a semantic carrier. Moves share one logical change ID and identify source and destination sides separately. HTML uses semantic elements such as `` and `` where the HTML content model allows them. Revised table and list nodes carry change attributes when a wrapper would be invalid. Markdown uses the documented CommonMark/GFM subset and raw semantic HTML when Markdown syntax cannot represent a structural revision without ambiguity. ## Choose the story and scope [#choose-the-story-and-scope] Omitting `in` addresses the body story. Pass a supported `StoryLocator` to read a header, footer, footnote, or endnote. Within that story, omit `scope` for the whole story, pass a public block address for one block, or pass a contiguous `SelectionTarget` for a range: ```ts const result = await doc.projectMarkdown({ in: { kind: 'story', storyType: 'footnote', noteId: '2' }, reviewMode: 'original', scope: { kind: 'selection', start: { kind: 'text', blockId: 'paragraph-a', offset: 4 }, end: { kind: 'text', blockId: 'paragraph-b', offset: 12 }, coordinateSpace: 'tracked', }, includeSourceMap: true, }); ``` Text offsets are UTF-16 code units. A visible-coordinate selection counts the chosen rendered review view. A tracked-coordinate selection also addresses deletion-side text. The resolved scope in the result always records the canonical tracked target used for the projection. Range projection fails closed when a boundary would cut through a table, field, or another structure whose partial serialization would be misleading. Invalid or missing targets remain typed input/address errors; the projection does not clip them into a different range. ## Inspect status and fidelity [#inspect-status-and-fidelity] Check `outcome`, `status`, `lossy`, and `diagnostics` before consuming `content`. `outcome` distinguishes preserved, warning-bearing, simplified, and rejected projections using the same vocabulary as rich input checks: | Status | Meaning | | --------- | -------------------------------------------------------------------------------------------------- | | `success` | No lossy diagnostic or fatal error | | `warning` | Usable output exists, and at least one diagnostic records a lossy fallback or placeholder | | `failed` | A source, scope, or representation condition prevented a truthful projection; output data is empty | Diagnostic codes and structured fields are the stable integration surface. Message prose can change. Each diagnostic names the source construct, disposition, story, applicable public IDs, and an output range when one exists. Diagnostics do not include document excerpts or raw package markup. The projection contract preserves semantic document content; it is not a DOCX layout renderer or a complete Word-file round trip: | Source construct | HTML projection | Markdown projection | | ------------------------------------------------ | --------------------------------------------------- | ----------------------------------------------------- | | Paragraphs, headings 1-6, baseline marks | Semantic HTML | CommonMark/GFM syntax; raw `` for underline | | Lists with exactly resolved Word labels | Nested lists with explicit visible-label carriers | Native syntax when exact; otherwise raw semantic HTML | | Missing or unresolved list labels | Readable `[list label unavailable]` placeholder | The same raw-HTML placeholder | | Rectangular, simple tables | Semantic table | GFM table | | Row/column spans or structurally rich tables | Semantic table with spans | Raw semantic HTML table | | Safe links and visible field results | Link or visible result | Link or visible result | | Images with a public resolvable URL | Image element | Markdown image | | Package-only media, drawings, OLE, embedded data | Readable deterministic placeholder and warning | Readable deterministic placeholder and warning | | Math, content controls, unsupported field shape | Preserved readable content or diagnosed placeholder | Preserved readable content or diagnosed placeholder | | Section, page, and column breaks | Semantic marker or diagnosed placeholder | Semantic marker or diagnosed placeholder | Exact Word numbering is resolved per effective list level, including legal numbering and overrides. HTML emits the visible label explicitly instead of relying on a browser's list counter. Markdown uses native list syntax only when it can reproduce that label and separator; custom, Roman, alphabetic, legal, tab-suffixed, or no-suffix labels can use the documented raw-HTML list carrier. This is a deterministic semantic representation, not a claim of Word visual parity. Use [`extract()`](/document-api/reference/extract) when the application needs the structured SDM snapshot rather than a serialized review view. Keep the DOCX itself when the workflow requires package parts, layout, macros, embedded objects, or Word-specific semantics that the construct matrix diagnoses or replaces. ## Map output back to document content [#map-output-back-to-document-content] Every detailed result contains `blocks`. Block ranges cover the complete serialized carrier and use UTF-16 offsets into `content`. A block with `identity: 'public'` includes the ID accepted by block APIs. A structurally emitted table, row, or cell that only had a positional source locator reports `identity: 'unavailable'` instead of presenting that locator as a stable public ID. Nested block ranges can overlap. Set `includeSourceMap: true` for fine-grained citations. Text entries map escaped output payload ranges to public `TextTarget` values in tracked coordinates. HTML/Markdown delimiters are intentionally unmapped. Synthetic list labels and placeholders have output ranges but no invented text target. ```ts const projection = await doc.projectHtml({ reviewMode: 'redline', includeSourceMap: true, }); const outputOffset = projection.content.indexOf('termination'); const entry = projection.sourceMap?.entries.find( (candidate) => candidate.kind === 'text' && candidate.output.start <= outputOffset && outputOffset < candidate.output.end, ); if (entry?.kind === 'text') { const current = await doc.info({}); if (current.revision !== projection.evaluatedRevision) { throw new Error('Projection map is stale; project the document again.'); } await doc.comments.create( { text: 'Review this source passage.', target: entry.source }, { expectedRevision: projection.evaluatedRevision }, ); } ``` `sourceMap.outputCoordinateSpace` is `utf16`; `sourceMap.sourceCoordinateSpace` is `tracked`. Escaped output remains atomic: for example, the full HTML entity `&` can map to one source `&` code unit. Store `evaluatedRevision` with every saved map. Any mutation makes the map and its block ranges stale, even when a familiar paragraph ID still exists. ## Handle comments and tracked-change annotations [#handle-comments-and-tracked-change-annotations] `annotations` is present even when the fine source map is omitted. Each comment or tracked change reports its public ID, public block IDs when available, output ranges, side, and one of these statuses: | Annotation status | Meaning | | ------------------ | --------------------------------------------------------------------------------- | | `emitted` | All representable anchored content for the chosen view and scope was emitted | | `partiallyEmitted` | Only part of a discontinuous or scope-clipped anchor survived | | `omitted` | The view removed the anchor, or its construct could not be represented truthfully | A comment on inserted content is omitted in `original`; a comment on deleted content is omitted in `final`. In `redline`, both sides are represented when their structure permits it. An omitted annotation has no output range and uses `omittedReason: 'reviewMode'` or `'unsupported'`. Do not guess an anchor by searching nearby rendered text; use its `sourceTarget` when present or project a view in which the anchor is emitted. ## Compatibility notes [#compatibility-notes] V2 accepts the deprecated `unflattenLists` option on `getHtml()` for source compatibility but ignores it. Omitted, `true`, and `false` all produce canonical nested lists. Detailed `projectHtml()` does not accept the option. See the generated [`projectHtml()`](/document-api/reference/project-html) and [`projectMarkdown()`](/document-api/reference/project-markdown) references for the exact input and result schemas. --- # Query document content > Find text in an open DOCX and keep mutation-ready targets. Use `doc.query.match()` when code needs to locate document content before reading or changing it. The query inspects the document model and returns explicit targets. It does not change the document. This guide starts with a mounted v2 editor. Complete the [Editor quickstart](/editor/quickstart) first if you do not yet have a working `SuperDoc` instance. ## 1. Query after the editor is ready [#1-query-after-the-editor-is-ready] Get the browser Document API from `superdoc.activeEditor.doc`. Query inside `onReady` so the document is available: ```ts import { SuperDoc } from 'superdoc'; import 'superdoc/style.css'; const superdoc = new SuperDoc({ selector: '#editor', document: '/contract.docx', onReady: async ({ superdoc }) => { const doc = superdoc.activeEditor?.doc; if (!doc) throw new Error('The active document is unavailable.'); const result = await doc.query.match({ select: { type: 'text', pattern: 'Confidential Information', }, require: 'all', }); console.log(`Found ${result.total} matches.`); for (const item of result.items) { if (item.matchKind !== 'text') continue; console.log(item.snippet, item.target, item.handle.ref); } }, }); window.addEventListener('beforeunload', () => { superdoc.destroy(); }); ``` This source is typechecked against the public v2 browser API. The default text selector performs a case-insensitive literal match. Use `mode: 'regex'` only when a literal pattern cannot express the search. > **Diagram:** Highlighted text in a DOCX becomes query result items with snippets, targets, and references. ## 2. Read the result [#2-read-the-result] The result separates human-readable context from mutation-ready locations: | Field | What it tells you | | --------------------- | ----------------------------------------------------------------- | | `total` | Number of matches before pagination | | `items` | Matches returned on this page | | `evaluatedRevision` | Document revision used to evaluate the query | | `item.snippet` | Matched text with nearby context | | `item.highlightRange` | Location of the match inside the snippet | | `item.target` | Direct target for one operation such as `replace()` or `delete()` | | `item.handle.ref` | Reference for a mutation plan tied to `evaluatedRevision` | Use `item.matchKind` before reading text-only fields. Node queries return a different item shape. ## 3. Choose the expected number of matches [#3-choose-the-expected-number-of-matches] Set `require` according to what makes the operation safe: | Value | Behavior | | -------------- | ------------------------------------------------- | | `'first'` | Return only the first match | | `'exactlyOne'` | Require one match and fail when the count differs | | `'all'` | Return all matches and fail when none exist | | `'any'` | Return all matches, including an empty result | Prefer `'exactlyOne'` when a later mutation must affect one unique clause. Prefer `'all'` when the task intentionally handles every occurrence. ## 4. Keep targets revision-safe [#4-keep-targets-revision-safe] A target or reference belongs to the document revision that produced it. If another operation changes the document first, run the query again before using an earlier target. For one direct operation, keep `item.target`. For a mutation plan, keep `item.handle.ref` together with `result.evaluatedRevision` so the plan can reject stale input instead of editing the wrong content. Pending tracked deletions are excluded from text queries by default. Set `includeDeletedText: true` only when the workflow explicitly needs to inspect deleted text. > **Verification target (success)** > > `result.total` should be greater than zero, each text item should include the search phrase in its snippet, and each > item should provide both a target and a reference. Next, [replace and delete content](/document-api/replace-delete-content) with fresh query targets, or review the [Document API mental model](/document-api/mental-model) for the full operation lifecycle. The [generated `query.match` reference](/document-api/reference/query/match) lists every selector and failure mode. --- # Receipts and errors > Check capabilities, inspect mutation results, and recover safely from stale document state. Treat every Document API mutation result as part of the operation contract. A successful receipt confirms what the engine applied. A failure receipt or rejected call explains why the workflow must stop, re-query, or change its request. ## 1. Check capabilities before acting [#1-check-capabilities-before-acting] Capabilities describe the current document runtime. Check the operation and the requested mutation mode before presenting or running a workflow: ```ts const capabilities = await doc.capabilities(); const replaceCapability = capabilities.operations.replace; if (!replaceCapability.available) { const reasons = replaceCapability.reasons?.join(', ') ?? 'No reason reported'; throw new Error(`Replace is unavailable: ${reasons}`); } if (!replaceCapability.tracked) { throw new Error('This document runtime cannot record replacements as tracked changes.'); } ``` Each operation capability reports `available`, `tracked`, and `dryRun`. Namespace-level flags such as `capabilities.global.trackChanges.enabled` describe broader runtime support. A capability check is a snapshot, not a guarantee. Document state, permissions, or targets can change before the mutation runs, so always inspect the eventual result too. ## 2. Handle both failure paths [#2-handle-both-failure-paths] Mutations can fail in two ways: 1. The call returns a receipt with `success: false` and `failure.code`. 2. The call rejects before applying anything, for example when input validation or a revision guard fails. These errors expose a machine-readable `code` and a message. ```ts function readErrorCode(error: unknown): string | undefined { if (typeof error !== 'object' || error === null || !('code' in error)) return; return typeof error.code === 'string' ? error.code : undefined; } const result = await doc.query.match({ select: { type: 'text', pattern: 'Amazing' }, require: 'exactlyOne', }); const match = result.items[0]; if (!match || match.matchKind !== 'text') { throw new Error('The company name was not found.'); } try { const receipt = await doc.replace( { target: match.target, text: 'Northstar' }, { changeMode: 'tracked', expectedRevision: result.evaluatedRevision }, ); if (!receipt.success) { console.error(receipt.failure?.code, receipt.failure?.message); return; } console.log('Mutation applied:', receipt); } catch (error) { console.error(readErrorCode(error), error); } ``` Do not treat the absence of an exception as success. Read `receipt.success` before saving, exporting, or starting a dependent operation. > **Diagram:** A successful mutation continues, while failures either re-query current state for one retry or stop so the request can change. ## 3. Choose recovery from the code [#3-choose-recovery-from-the-code] Use the code to select a bounded recovery path: | Code family | Meaning | Action | | -------------------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------- | | `REVISION_MISMATCH`, `STALE_REVISION`, `ADDRESS_STALE`, `TARGET_NOT_FOUND` | The document or target changed | Re-query current state, review the new match, and retry once | | `NO_OP` | The request would not change the document | Stop and verify whether the desired state already exists | | `CAPABILITY_UNAVAILABLE`, `CAPABILITY_UNSUPPORTED` | The current runtime cannot perform the request | Change the mode, operation, or runtime; do not retry unchanged | | `PERMISSION_DENIED` | Document policy prevents the mutation | Keep the document unchanged and resolve authorization or protection first | | `INVALID_INPUT`, `INVALID_TARGET` | The request shape or target is invalid | Fix the request; do not retry the same payload | | `INTERNAL_ERROR` | The runtime could not complete the operation safely | Record the code and context, then stop the workflow | Not every operation can return every code. Use the generated operation reference when implementing operation-specific recovery. ## 4. Retry state drift once [#4-retry-state-drift-once] When a revision or target is stale, run the original query again and review its current result before retrying. Limit automatic retries to one. A repeated state-drift failure usually means another writer is active or the workflow is targeting unstable content. Never remove `expectedRevision` just to make a retry pass. That guard prevents a valid operation from applying to an unintended document state. > **Keep failures observable (warning)** > > Log the operation name, failure code, and a safe correlation identifier. Do not log full document contents, private > clauses, or complete mutation payloads by default. Continue with [Replace and delete content](/document-api/replace-delete-content) for a complete revision-guarded example. The [generated Document API reference](/document-api/reference) lists each operation's receipt and failure codes. --- # Replace and delete content > Use fresh query targets to change an open DOCX and inspect each mutation receipt. Use `replace()` and `delete()` after a query has identified the exact content to change. Both operations accept a target from `query.match()` and return a receipt that says whether the mutation succeeded. For HTML, Markdown, or `SDFragment` replacement, see [insert HTML and Markdown](/document-api/rich-content). Rich replacement uses the same fresh-target and revision-guard rules described here. To replace every block in the main body, pass the explicit body target. This is a Document API content mutation: it keeps the open DOCX package and its terminal page setup while replacing the body blocks in one undo step. ```ts const body = { kind: 'story', storyType: 'body' } as const; const receipt = await doc.replace( { target: body, type: 'markdown', value: '# Replacement\n\nNew body.' }, { changeMode: 'direct', expectedRevision }, ); ``` The body target also accepts `text` or a preconverted `content` fragment. It is main-body-only and direct-mode-only. Tracked whole-body replacement fails with `CAPABILITY_UNSUPPORTED`; use a selection or block target when the replacement must remain a review suggestion. To open a different DOCX package, use the editor's file replacement flow instead. This guide uses the [tracked-changes fixture](/fixtures/tracked-changes.docx). Serve it from `/contract.docx` in your app, or update the document URL in the example. ## 1. Run two targeted mutations [#1-run-two-targeted-mutations] Wait for the editor, replace one company name, then run a fresh query before deleting a sentence: ```ts import { SuperDoc } from 'superdoc'; import 'superdoc/style.css'; const superdoc = new SuperDoc({ selector: '#editor', document: '/contract.docx', onReady: async ({ superdoc }) => { const doc = superdoc.activeEditor?.doc; if (!doc) throw new Error('The active document is unavailable.'); const companyMatch = await doc.query.match({ select: { type: 'text', pattern: 'Amazing' }, require: 'exactlyOne', }); const company = companyMatch.items[0]; if (!company || company.matchKind !== 'text') { throw new Error('The company name was not found.'); } const replaceReceipt = await doc.replace( { target: company.target, text: 'Northstar', }, { changeMode: 'direct', expectedRevision: companyMatch.evaluatedRevision, }, ); if (!replaceReceipt.success) { throw new Error(`Replace failed: ${replaceReceipt.failure?.message ?? 'Unknown error'}`); } console.log('Replace receipt:', replaceReceipt); const liabilityMatch = await doc.query.match({ select: { type: 'text', pattern: 'The total liability under this section shall not exceed $500,000.', }, require: 'exactlyOne', }); const liability = liabilityMatch.items[0]; if (!liability || liability.matchKind !== 'text') { throw new Error('The liability sentence was not found.'); } const deleteReceipt = await doc.delete( { target: liability.target, behavior: 'exact', }, { changeMode: 'direct', expectedRevision: liabilityMatch.evaluatedRevision, }, ); if (!deleteReceipt.success) { throw new Error(`Delete failed: ${deleteReceipt.failure.message}`); } console.log('Delete receipt:', deleteReceipt); }, }); window.addEventListener('beforeunload', () => { superdoc.destroy(); }); ``` The displayed source is typechecked against the public v2 browser API. ## 2. Re-query after a mutation [#2-re-query-after-a-mutation] The replacement advances the document revision. The example runs a new query before deleting instead of reusing an earlier target. `expectedRevision` makes each operation reject stale query input. `changeMode: 'direct'` applies the change immediately. Use `'tracked'` when a change should remain a suggestion for human review. `behavior: 'exact'` removes only the resolved text range. The default `'selection'` behavior may expand to block edges when the query covers an entire boundary block. ## 3. Inspect receipts before continuing [#3-inspect-receipts-before-continuing] Check `success` before saving, exporting, or starting another dependent operation. A failed receipt includes a stable failure code and message. A successful receipt includes the resolved target, and may include revision and effect details depending on the operation. Common failures at this stage mean the target is stale, the text no longer matches, or the current editor mode does not allow the mutation. Re-query current document state before retrying. Do not guess offsets or silently ignore the receipt. > **Verification target (success)** > > The mounted document should show `Northstar Corp` instead of `Amazing Corp`, and the liability sentence should be > absent. Both receipts should report `success: true`. Next, learn how to handle [receipts and errors](/document-api/receipts-and-errors) without unsafe retry loops. For several edits that must succeed together, use a revision-guarded mutation plan instead of chaining independent calls. The generated [replace reference](/document-api/reference/replace) and [delete reference](/document-api/reference/delete) list the full input and receipt shapes. --- # Insert HTML and Markdown > Convert, inspect, and apply bounded HTML or Markdown content to an open DOCX. The Document API accepts HTML and Markdown as structured input. You can inspect the canonical fragment first, or pass the source directly to `insert()` or `replace()`. HTML and Markdown are not Word package fragments. SuperDoc preserves the supported structure listed below, normalizes representation details, and reports anything it drops or downgrades. ## Convert before applying [#convert-before-applying] Use `htmlToFragment()` or `markdownToFragment()` when your application needs to inspect diagnostics or reuse the canonical fragment: ```ts import { SuperDoc } from 'superdoc'; import 'superdoc/style.css'; const superdoc = new SuperDoc({ selector: '#editor', document: '/contract.docx', onReady: async ({ superdoc }) => { const doc = superdoc.activeEditor?.doc; if (!doc) throw new Error('The active document is unavailable.'); const converted = await doc.htmlToFragment({ html: '

Scope

Review this clause.

', }); const fatal = converted.diagnostics.find((diagnostic) => diagnostic.severity === 'error'); if (fatal) throw new Error(`HTML conversion failed: ${fatal.message}`); const insertInput = { type: 'html', value: '

Scope

Review this clause.

', } as const; const checked = await doc.capabilities.check({ operation: 'insert', input: insertInput, options: { changeMode: 'tracked' }, }); if (checked.operation !== 'insert') { throw new Error(`Unexpected support-check operation: ${checked.operation}`); } if (!checked.supported || !checked.guard) { throw new Error(`Rich insert is not supported: ${checked.failure?.message ?? checked.outcome}`); } const insertReceipt = await doc.insert(insertInput, { changeMode: 'tracked', supportCheck: checked.guard, }); if (!insertReceipt.success) { throw new Error(`Rich insert failed: ${insertReceipt.failure?.message ?? 'unknown failure'}`); } const match = await doc.query.match({ select: { type: 'text', pattern: 'Existing clause' }, require: 'exactlyOne', }); const clause = match.items[0]; if (!clause || clause.matchKind !== 'text') throw new Error('The clause was not found.'); const replaceReceipt = await doc.replace( { target: clause.target, type: 'markdown', value: '**Replacement clause** with a [reference](https://example.com/policy).', }, { changeMode: 'tracked', expectedRevision: match.evaluatedRevision, }, ); if (!replaceReceipt.success) { throw new Error(`Rich replace failed: ${replaceReceipt.failure?.message ?? 'unknown failure'}`); } console.log(checked.outcome, insertReceipt.outcome, replaceReceipt.conversion); }, }); window.addEventListener('beforeunload', () => { superdoc.destroy(); }); ``` Each result contains: * `fragment`: the canonical structured content. * `lossy`: `true` when conversion normalized, downgraded, or dropped source content. * `diagnostics`: ordered, source-located conversion findings. A warning-bearing result can still be applied. An error means there is no safe rich fragment to apply. Check diagnostic `severity` and `disposition` instead of treating every lossy result as a failure. ## Check an exact workflow before applying [#check-an-exact-workflow-before-applying] Use `doc.capabilities()` for a cheap synchronous snapshot of general operation availability. Use `doc.capabilities.check()` when the answer must account for exact HTML or Markdown, the current target, the requested change mode, and the current document revision. A check does not mutate package bytes, history, tracked changes, or the public revision. A supported write that would change the document returns a guard. Pass it to the unchanged rich `insert()` or `replace()` request. The write fails closed when the source, format, target, placement, story, nesting policy, mode, analysis result, or document revision no longer matches. The guard digest detects accidental reuse; it is not a security signature. The common `outcome` is `preserved`, `preserved-with-warnings`, `simplified`, `rejected`, `no-op`, `invalid-target`, or `outdated`. Read the complete conversion diagnostics and final mutation receipt before choosing application-specific fallback behavior. ## Insert or replace raw source [#insert-or-replace-raw-source] Pass `value` with `type: 'html'` or `type: 'markdown'` to convert and apply in one call. Rich insert supports a selection, a block with `before` or `after`, a ref, or no target for append. Rich replace accepts a selection, paragraph or heading block, whole table block, ref, or the explicit `{ kind: 'story', storyType: 'body' }` target. The body target replaces all main-body blocks in one direct mutation while retaining the destination DOCX package and terminal section properties. It does not replace the DOCX file. Tracked mode is supported for selection and block workflows, but tracked whole-body replacement returns `CAPABILITY_UNSUPPORTED`. Use `dryRun: true` to resolve and preflight without creating IDs, history, or document changes. Pair a mutation with `expectedRevision` when it depends on an earlier read. Successful raw-source receipts include the final `outcome` and a `conversion` report. They may also include created entity identities, source-path effects, affected stories, text-range shifts, and a transaction ID. Check `success` before using any success-only field. ## Supported inbound constructs [#supported-inbound-constructs] | Source construct | Result | | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | Paragraphs, headings, bold, italic, underline, strike, hard breaks | Preserved as canonical blocks and runs | | Ordered and unordered lists, including bounded nesting and ordered starts | Preserved with canonical list levels | | Tables with a complete leading header row and column spans | Preserved; explicit widths and broad table styling are not | | Safe `http`, `https`, and `mailto` links | Preserved as external hyperlinks | | Horizontal rules | Preserved as durable horizontal-rule blocks | | Safe bounded color, font, spacing, indentation, cell shading, and padding | Normalized to typed canonical properties | | Code, block quotes, task markers, and unsupported inline forms | Downgraded or dropped with diagnostics | | Scripts, event handlers, unsafe URLs, raw images, nested tables, row spans, malformed or over-limit input | Sanitized with warnings when safe structure remains; otherwise rejected | The limits are deliberate. Arbitrary CSS, embedded active content, and full DOCX package fidelity are outside the HTML/Markdown contract. ## Projection and persistence [#projection-and-persistence] Direct changes are immediately part of the document. This inbound API creates review markup that can be accepted or rejected through the tracked-changes API. Detailed outbound HTML and Markdown reads can then project `redline`, `final`, or `original` views. Save and reopen the DOCX before treating a mutation workflow as complete. Standalone outbound conversion of an `SDFragment` to HTML or Markdown is not part of this inbound API. Document reads have a separate [HTML and Markdown projection contract](/document-api/output-projections), including compact string getters and detailed reads with review, fidelity, and provenance metadata. For target discovery, start with [query content](/document-api/query-content). For failure handling, see [receipts and errors](/document-api/receipts-and-errors). The generated [HTML conversion reference](/document-api/reference/html-to-fragment), [insert reference](/document-api/reference/insert), and [replace reference](/document-api/reference/replace) list the complete shapes. --- # Work with tracked changes > Create reviewable edits, inspect open changes, and accept or reject an explicit change. Tracked changes connect programmatic editing to human review. Operations that report tracked-mode support can request it. The review API then lists, inspects, accepts, or rejects the resulting logical changes. ## Create a reviewable edit [#create-a-reviewable-edit] Pass `changeMode: 'tracked'` as the mutation options argument. There is no separate operation for creating a tracked change: ```ts const result = await doc.query.match({ select: { type: 'text', pattern: 'Amazing' }, require: 'exactlyOne', }); const match = result.items[0]; if (!match || match.matchKind !== 'text') { throw new Error('The company name was not found.'); } const receipt = await doc.replace( { target: match.target, text: 'Northstar' }, { changeMode: 'tracked', expectedRevision: result.evaluatedRevision }, ); if (!receipt.success) { throw new Error(receipt.failure?.message ?? 'The tracked replacement failed.'); } ``` The replacement remains open for review. Saving the DOCX preserves that review state until a person or programmatic workflow accepts or rejects it. ## Review the result in the Editor [#review-the-result-in-the-editor] The Editor owns document rendering, selection, and human review controls. Follow [Review tracked changes](/editor/track-changes) to open the sample DOCX and accept or reject a proposal visually. The underlying tracked-change operations remain the same in Editor and Headless hosts. ## List and inspect changes [#list-and-inspect-changes] `trackChanges.list()` returns a compact, paginated result. Call `trackChanges.get()` when the workflow needs full before/after details for one logical change: ```ts const changes = await doc.trackChanges.list({ limit: 20, offset: 0 }); const change = changes.items[0]; if (!change) { throw new Error('The document has no open tracked changes.'); } const detail = await doc.trackChanges.get({ id: change.id }); console.log({ id: detail.id, type: detail.type, author: detail.author, before: detail.before, after: detail.after, }); ``` The list searches the document body by default. Pass `in: 'all'` when the workflow must include supported headers, footers, footnotes, and endnotes. ## Read final, original, or redline output [#read-final-original-or-redline-output] Use `projectHtml()` or `projectMarkdown()` when a workflow needs a serialized review view rather than the tracked-change catalog. `final` emits accepted-side content, `original` emits the before-side, and `redline` identifies representable before/after content with the same logical change IDs returned here. The view applies to structural paragraph, list, and table changes as well as inline text. Moves distinguish source and destination; formatting-only changes emit one content copy with an annotation carrier. The projection result records annotations removed by the selected view instead of silently dropping their identity. Source targets and maps use tracked coordinates and are valid only at `evaluatedRevision`. Read the [HTML and Markdown projection guide](/document-api/output-projections) before saving a map for a later review action. ## Accept or reject one change [#accept-or-reject-one-change] Decide against the logical change ID, and guard the decision with the revision returned by the list operation: ```ts const decisionReceipt = await doc.trackChanges.decide( { decision: 'accept', target: { kind: 'id', id: detail.id }, }, { expectedRevision: changes.evaluatedRevision, }, ); if (!decisionReceipt.success) { throw new Error(`${decisionReceipt.failure.code}: ${decisionReceipt.failure.message}`); } console.log('Resolved change:', decisionReceipt.removed); ``` Use `decision: 'reject'` to restore the change's before-state instead. A review decision resolves an existing change, so `changeMode` and `dryRun` do not apply. Re-list changes after each decision. The receipt can invalidate or remap references, and partial range decisions can create successor fragments with new IDs. > **Verification target (success)** > > After the decision succeeds, a fresh `trackChanges.list()` result should no longer contain the resolved logical change > ID. The saved DOCX should preserve the accepted or rejected document state. For a complete code-to-human handoff, follow [Review tracked changes](/agents/workflows/review-tracked-changes). The generated [`trackChanges` reference](/document-api/reference/track-changes) covers range, side, bulk, and story-specific targets. --- # Build accessible Editor experiences > Preserve keyboard access, names, focus, status announcements, and application-owned accessibility around SuperDoc. The built-in toolbar and surfaces provide keyboard and ARIA behavior, but the complete experience includes the application shell, custom controls, dialogs, validation, and save state. Accessibility remains a shared responsibility. ## Preserve the built-in contract [#preserve-the-built-in-contract] * Keep visible labels or accessible names when replacing toolbar text and icons. * Do not remove focus outlines without an equally visible replacement. * Give dialogs and floating surfaces a title, `ariaLabel`, or `ariaLabelledBy`. * Keep Escape and focus behavior predictable. Do not trap focus in a non-modal floating tool. * Let fit-to-width respond to zoom and container changes without preventing browser zoom. ## Own custom UI semantics [#own-custom-ui-semantics] Use native buttons, inputs, and selects whenever possible. Mirror command `active` with `aria-pressed`, disable unavailable actions, and announce async save or mutation outcomes through a polite live region. Return focus to the document when a temporary control closes and preserve the selection when a composer takes focus. Keyboard shortcut metadata does not install a listener. The application must bind shortcuts, prevent conflicts with browser and assistive-technology commands, and route every shortcut through the same command handle as its visible control. Test keyboard-only navigation, high contrast, 200% browser zoom, reduced motion, and at least one screen reader on supported browsers. Automated checks catch markup defects but do not prove that document editing is understandable. --- # Configure the Editor > Set how the Editor starts, then find the configuration options your integration needs. Continue with the `/sample.docx` project from the [Quickstart](/editor/quickstart). Add a current user and choose how the Editor starts. ## Set the startup configuration [#set-the-startup-configuration] Add `documentMode` and `user` to the Editor setup from Quickstart: **Vanilla — `Startup options`** ```ts import type { Config } from 'superdoc'; export const startupOptions = { documentMode: 'suggesting', user: { name: 'Jordan Lee', email: 'jordan@example.com', }, } satisfies Partial; ``` **React — `Startup options`** ```tsx import type { SuperDocEditorProps } from '@superdoc/react'; export const startupOptions = { documentMode: 'suggesting', user: { name: 'Jordan Lee', email: 'jordan@example.com', }, } satisfies Partial; ``` In Vanilla, spread `startupOptions` into the object passed to `new SuperDoc()`. In React, spread it onto `SuperDocEditor`. Keep the callbacks and export code from Quickstart. Reload the application and change `September 1, 2026` to `October 1, 2026`. The edit should appear as a tracked change instead of replacing the date directly. The `user` value identifies the author of the tracked change. This example uses a fixed user so it runs without authentication. `satisfies` checks the field names and values during typechecking. ## Find an option [#find-an-option] Choose a group, then choose a field. Each entry shows what it changes, its type, and its default. Some fields link to a guide with a complete example. Expand **API details** for the generated description. ### Essentials | Field | Type | Default | Status | Summary | API details | Guide | | --- | --- | --- | --- | --- | --- | --- | | `selector` | `string \| HTMLElement` | — | Required | Choose the element where the Editor mounts. | The selector or element to mount the SuperDoc into. | — | | `document` | `DocumentSource \| null` | — | Optional | Open a document from a URL, File, Blob, or collaboration source. | Document to open. Pass a URL, file, byte source, or structured source. Use a structured document carrying `collaboration` for collaboration, or a structured source for other metadata. Omit it to open a blank DOCX. | [Load and save documents](/editor/load-and-save-documents) | | `documentMode` | `"editing" \| "viewing" \| "suggesting"` | `'editing'` | Optional | Start in editing, suggesting, or viewing mode. | The mode of the document (default: 'editing'). | [Document modes](/editor/document-modes) | | `user` | `{ color?: string; id?: null \| string; name?: null \| string; email?: null \| string; image?: null \| string; }` | — | Optional | Identify the current user for collaboration and tracked changes. | The current user of this SuperDoc. Typed as `AwarenessUser` (an extension of `User` with the optional `color` field) so consumers can pass an explicit awareness color and have the runtime honor it as an override - `SuperDoc#assignUserColor()` skips its hash-based assignment when `user.color` is already set. | — | ### Document | Field | Type | Default | Status | Summary | API details | Guide | | --- | --- | --- | --- | --- | --- | --- | | `viewing` | `{ comments?: boolean; trackedChanges?: "original" \| "markup" \| "final"; }` | `{ comments: false, trackedChanges: 'original' }` | Optional | Choose what comments and tracked changes viewers see. | What review information is shown when `documentMode` is `viewing`. | [Document modes](/editor/document-modes) | | `role` | `"editor" \| "viewer" \| "suggester"` | — | Optional | Limit which document modes the current user can enter. | The role of the user in this SuperDoc. | — | | `allowSelectionInViewMode` | `boolean` | `false` | Optional | Let viewers select text without editing. | When `documentMode` is `'viewing'`, allow the user to make text selections even though editing is disabled. Defaults to `false`. Forwarded to the underlying editor as `options.allowSelectionInViewMode`. | [Document modes](/editor/document-modes) | | `superdocId` | `string` | — | Optional | Set an ID for this Editor instance. | The ID of the SuperDoc. | — | | `password` | `string` | — | Optional | Open an encrypted DOCX with its password. | Password for encrypted DOCX files. Forwarded during document load. | — | | `documents` | `Document[]` | — | Optional | Load documents through the legacy multi-document field. | Documents to load. | — | | `users` | `User[]` | — | Optional | Provide the people available for mentions. | All users of this SuperDoc (can be used for "@"-mentions). | — | | `colors` | `string[]` | — | Optional | Provide awareness colors for users. | Colors to use for user awareness. | — | | `title` | `string` | — | Optional | Set the fallback filename used when exporting. | Fallback filename for `export()` when `exportedName` is omitted. | — | ### Interface | Field | Type | Default | Status | Summary | API details | Guide | | --- | --- | --- | --- | --- | --- | --- | | `ui` | `false \| { toolbar?: false \| true \| ToolbarConfig; comments?: false \| true \| CommentsConfig; contextMenu?: false \| true \| ContextMenuConfig; loading?: boolean; search?: false \| true \| SearchConfig; linkPopover?: false \| true \| LinkPopoverConfig; ruler?: false \| true \| RulerConfig; contentControls?: false \| true \| ContentControlsConfig; }` | — | Optional | Choose which built-in interface parts SuperDoc renders. | Which built-in interface SuperDoc renders. Omit it to keep SuperDoc's historical rendering: comments, the context menu, content-control chrome, and mode-aware hyperlink activation are on; search and the ruler are opt-in; and the toolbar renders once it has somewhere to mount. That profile is not symmetrical, and omitting this field reproduces it exactly. Pass `false` when the application owns the interface. SuperDoc then renders no controls, chrome, dialogs, or popovers, while the document, the Document API, and `superdoc.ui` keep working — so a custom UI drives the same commands the built-in one would have. Pass an object to choose per surface. An omitted key keeps that surface's default rather than following its siblings, so `{ comments: false }` disables comments and changes nothing else. | [Choose your interface](/editor/who-renders-the-ui) | | `interaction` | `{ comments?: { level?: CommentInteractionLevel; }; trackedChanges?: { allowDecisions?: boolean; }; }` | — | Optional | Set what people can do through Editor interactions. | Client-side interaction policy. Independent of `ui`, so it still applies when the application renders its own UI. This is not an authorization boundary. | [Choose your interface](/editor/who-renders-the-ui) | | `surfaces` | `{ resolver?: null \| (request: SurfaceRequest) => SurfaceResolution \| null \| undefined; dialog?: { closeOnEscape?: boolean; closeOnBackdrop?: boolean; maxWidth?: string \| number; }; floating?: { placement?: SurfaceFloatingPlacement; width?: string \| number; maxWidth?: string \| number; maxHeight?: string \| number; closeOnEscape?: boolean; closeOnOutsidePointerDown?: boolean; autoFocus?: boolean; }; }` | — | Optional | Configure dialogs and floating overlays. | Shared configuration for dialogs and floating overlays, including ones opened through `superdoc.openSurface()`. Stays active under `ui: false`. | [Dialogs and surfaces](/editor/dialogs-and-surfaces) | | `uiDisplayFallbackFont` | `string` | — | Optional | Set the font used by SuperDoc interface elements. | The font-family to use for all SuperDoc UI surfaces (toolbar, comments UI, dropdowns, tooltips, etc.). This ensures consistent typography across the entire application and helps match your application's design system. The value should be a valid CSS font-family string. Example (system fonts): uiDisplayFallbackFont: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' Example (custom font): uiDisplayFallbackFont: '"Inter", Arial, sans-serif' | — | ### Behavior | Field | Type | Default | Status | Summary | API details | Guide | | --- | --- | --- | --- | --- | --- | --- | | `hyperlinks` | `false \| { onActivate?: (context: HyperlinkActivationContext) => HyperlinkActivationResult \| null \| undefined; }` | — | Optional | Choose what happens when a person activates a hyperlink. | Hyperlink activation behavior. By default, editable links open the built-in hyperlink editor in Editing or Suggesting mode. In Viewing mode, and for links outside editable text, SuperDoc follows the URL or document anchor. Pass `false` to suppress activation in every mode. A configured `onActivate` handler stays active with `ui: false`, allowing a custom interface to handle hyperlinks. | [Hyperlinks](/editor/built-in-ui/hyperlinks) | | `isLocked` | `boolean` | — | Optional | Set the initial shared lock metadata. | Initial shared lock metadata. This value does not make the document read-only. Use `documentMode` or interaction policy to restrict editing in the client. | — | | `lockedBy` | `{ id?: null \| string; name?: null \| string; email?: null \| string; image?: null \| string; }` | — | Optional | Identify the user who locked the Editor. | User associated with the initial shared lock metadata. | — | | `viewOptions` | `{ layout?: "print" \| "web"; }` | — | Optional | Set DOCX-compatible document view options. | Document view options (OOXML ST_View compatible). | — | | `contained` | `boolean` | `false` | Optional | Keep the Editor inside a fixed-height scrolling container. | Enable contained mode for fixed-height container embedding. SuperDoc supports two layout modes, and the host element's height requirement differs between them: - Natural (default, `false`): the Editor grows to the document's full height and the page scrolls. The host needs no height. Setting one does not constrain the document or enable internal scrolling, because SuperDoc leaves overflow visible in this mode, though application CSS on the host can still clip what is drawn. - Contained (`true`): SuperDoc propagates `height: 100%` through its DOM tree and scrolls the document internally, so multi-page documents stay inside the host. This mode requires the host to have a definite height (for example `height: 400px`); without one there is nothing for the percentage heights to resolve against. A toolbar mounted through `Config.toolbar` or `modules.toolbar.selector` is never part of this calculation. Placed as a sibling of the host, its height adds to the host's: a 400px host with a 40px toolbar occupies 440px in total. Placed inside the host, it consumes part of the 400px instead. | [Responsive layout](/editor/built-in-ui/responsive-layout) | | `zoom` | `{ initial?: number; mode?: "manual" \| "fit-width"; fitWidth?: SuperDocFitWidthOptions; }` | `{ initial: 100, mode: 'manual' }` | Optional | Set the initial zoom and fit-to-width behavior. | Zoom behavior: the initial zoom level and optional fit-width policy. See `SuperDocZoomConfig`. | [Responsive layout](/editor/built-in-ui/responsive-layout) | | `measurementUnit` | `"in" \| "cm"` | `'in'` | Optional | Set the ruler and measurement unit. | Starting measurement unit for rulers and measurement fields (Word's "measurement units" preference). Defaults to `'in'` (Word's en-US default). Change it at runtime with `setMeasurementUnit()`. See `SuperDocMeasurementUnit`. | [Ruler](/editor/built-in-ui/ruler) | ### Integrations | Field | Type | Default | Status | Summary | API details | Guide | | --- | --- | --- | --- | --- | --- | --- | | `modules` | `{ trackChanges?: TrackChangesModuleConfig; }` | — | Optional | Configure document features that do not belong to the built-in interface. | Modules to load. | [Track changes](/editor/track-changes) | | `permissionResolver` | `(params: { permission: string; role: string; isInternal: boolean; defaultDecision: boolean; comment: null \| object; trackedChange: null \| object; currentUser: null \| User; superdoc: null \| SuperDocClass; }) => boolean \| undefined` | — | Optional | Customize client-side permission decisions. | Customize client-side permission decisions. This is not an authorization boundary. When both resolver spellings are present, this field takes precedence over the deprecated `modules.comments.permissionResolver` field. | — | | `extensions` | `SuperDocExtension>[]` | — | Optional | Add extensions created with `defineSuperDocExtension`. | SuperDoc v2 extensions created with `defineSuperDocExtension`. These extensions activate without an `editorVersion` or `editorIntegration` selector. Each extension owns isolated storage, named events, commands, anchors, and render-only decorations, and mutates the document exclusively through the guarded Document API (`ctx.doc.*`). This is the v2 replacement for the v1/ProseMirror `editorExtensions` path; the two are not interchangeable. Extension arrays are mount-time config: changing the array reference requires a remount to take effect. | — | | `handleImageUpload` | `(file: { lastModified: number; name: string; webkitRelativePath: string; size: number; type: string; arrayBuffer: () => Promise; bytes: () => Promise>; slice: (start: number \| undefined, end: number \| undefined, contentType: string \| undefined) => Blob; stream: () => ReadableStream>; text: () => Promise; }) => Promise` | — | Optional | Store images inserted into the document. | The function to handle image uploads. | — | | `cspNonce` | `string` | — | Optional | Apply a Content Security Policy nonce to SuperDoc runtime styles. | Content Security Policy nonce for SuperDoc runtime styles. Editors that share a document must use the same nonce. | [Secure integration](/editor/secure-integration) | | `licenseKey` | `string` | — | Optional | Set the client-visible license identity sent with document-open telemetry. | Client-visible license identity sent with document-open telemetry. | [License](/editor/license) | | `telemetry` | `{ enabled: boolean; endpoint?: string; metadata?: Record; }` | `{ enabled: true }` | Optional | Configure telemetry sent when a DOCX becomes ready. | Document-open telemetry settings. Enabled by default. | [Telemetry](/editor/telemetry) | | `proofing` | `ProofingConfig` | — | Optional | Configure spelling and grammar checks. | Proofing / spellcheck configuration. | [Add proofing](/editor/platform/proofing) | | `fonts` | `{ bundled?: false \| true \| string[] \| Record \| "baseline" \| "full"; families?: FontFamilyConfig[]; assetBaseUrl?: string; resolveAssetUrl?: (context: import("@superdoc/font-system").FontAssetUrlContext) => string; assetUrl?: string \| (context: import("@superdoc/font-system").FontAssetUrlContext) => string; }` | — | Optional | Configure document fonts and font asset loading. | Font system configuration. The reviewed fallback pack ships in the optional `@superdoc-dev/fonts` package: pass `superdocFonts` (bundler) or the `SuperDocFonts` global from its `superdoc-fonts.min.js` browser build (CDN). To self-host, set `fonts.assetBaseUrl` (e.g. `/fonts/` or a CDN URL) or `fonts.resolveAssetUrl` for signed/versioned hosting. SuperDoc core ships no fonts; with none configured the toolbar shows the baseline and documents render with system fonts. | — | | `workerUrls` | `{ document?: string \| URL; collaboration?: string \| URL; reviewIndex?: string \| URL; }` | — | Optional | Load browser workers from same-origin URLs. | Optional same-origin URLs for v2's browser worker assets. Configure these when the application and SuperDoc bundle are served from different origins. Omitted entries keep SuperDoc's bundled worker URLs. | [Secure integration](/editor/secure-integration) | ### Lifecycle | Field | Type | Default | Status | Summary | API details | Guide | | --- | --- | --- | --- | --- | --- | --- | | `onReady` | `(params: { superdoc: SuperDocClass; }) => void` | — | Optional | Enable document actions after the Editor is ready. | Callback when the SuperDoc is ready. Receives a wrapper carrying the live SuperDoc instance. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onContentError` | `(params: { error: unknown; editor: Editor; documentId: string; file: null \| File \| Blob; }) => void` | — | Optional | Handle document import and content errors. | Called when the editor cannot read or update document content. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onException` | `(params: SuperDocExceptionPayload) => void` | — | Optional | Handle SuperDoc runtime exceptions. | Callback when SuperDoc emits an `exception` event. The payload is a union of runtime shapes (store init, restore failure, editor lifecycle, built-in toolbar, hyperlink activation, structured diagnostic). Narrow with `'stage' in params` (store init), `'code' in params` (editor), `'itemName' in params` (toolbar), `'source' in params` (hyperlink), or `'diagnosticCode' in params` (structured diagnostic) before reading shape-specific fields. A structured diagnostic (`SuperDocExceptionDiagnosticPayload`, `diagnosticCode` one of `PARSE_ERROR` \| `RENDER_ERROR` \| `UNSUPPORTED_FEATURE` \| `PERFORMANCE_ERROR`) can accompany a legacy exception payload. SuperDoc filters unsupported internal records. For translated package and readiness records, it emits at most one structured diagnostic for each `(documentId, generation, internalCode)` tuple. It also suppresses a generic boot diagnostic when a more specific package diagnostic describes the same failure. A single incident can therefore raise 0..N structured diagnostics. Only the `unzip` and `render` stages are populated today; `parse` and `layout` are reserved for future coverage. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onEditorCreate` | `(params: { editor: Editor; }) => void` | — | Optional | Run code after an editor is created. | Callback after an editor is created. Receives a wrapper carrying the editor. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onSourceComplete` | `() => void` | — | Optional | Run code when the document is ready for diff capture. | Callback when the v2 document source reaches source-complete posture and diff.capture is safe to call. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onSourceSignalsComplete` | `() => void` | — | Optional | Run code after source signals finish building. | Callback when v2 source signals finish building (fires after onSourceComplete; diff.capture is synchronously safe). | [Lifecycle and events](/editor/lifecycle-and-events) | | `onCommentsUpdate` | `(params: { type: string; comment?: Comment; changes?: { key: string; commentId: string; fileId?: string \| null; }[]; pendingSelection?: null \| SelectionInfo; }) => void` | — | Optional | React when comments change. | Callback when comments are updated. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onContentControlClick` | `(params: { target: ContentControlRef; source: "pointer"; }) => void` | — | Optional | React when a person selects a content control. | Callback when someone clicks inside a content control. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onAwarenessUpdate` | `(params: { states: AwarenessState[]; added: number[]; removed: number[]; superdoc: SuperDocClass; }) => void` | — | Optional | React when collaboration awareness changes. | Callback when awareness is updated. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onLocked` | `(params: { isLocked: boolean; lockedBy: null \| User; }) => void` | — | Optional | React when the Editor locks or unlocks. | Callback when the SuperDoc is locked or unlocked. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onPdfDocumentReady` | `() => void` | — | Optional | Run code when a PDF document is ready. | Callback when the PDF document is ready. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onSidebarToggle` | `(isOpened: boolean) => void` | — | Optional | React when the sidebar opens or closes. | Callback when the sidebar is toggled. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onCollaborationReady` | `(params: { editor: Editor; }) => void` | — | Optional | Enable shared-document actions when collaboration is ready. | Callback when collaboration is ready. Receives a wrapper carrying the editor. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onEditorUpdate` | `(params: { editor?: Editor; sourceEditor?: Editor; surface: "body" \| "header" \| "footer"; headerId: null \| string; sectionType: null \| string; }) => void` | — | Optional | React after document content changes. | Callback when document is updated. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onTrackedChangesBulkDecision` | `(params: { documentId: null \| string; decision: "accept" \| "reject"; requestedCount: number; successfulCount: number; permissionDeniedCount: number; }) => void` | — | Optional | React after Accept All or Reject All finishes, including permission-denied counts. | Callback after an Accept All or Reject All tracked-change decision. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onCommentsListChange` | `(params: { isRendered: boolean; }) => void` | — | Optional | React when the comments list is rendered. | Called when the built-in comments list is rendered or removed. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onPaginationUpdate` | `(params: { totalPages: number; superdoc: SuperDocClass; }) => void` | — | Optional | Read the page count after a layout update. | Called after each pagination layout pass with the current page count. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onZoomChange` | `(params: { zoom: number; mode: "manual" \| "fit-width"; }) => void` | — | Optional | React when the zoom level changes. | Callback when the zoom level changes. Fires for every zoom source: `setZoom()`, the toolbar zoom control, and fit-width adjustments. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onViewportChange` | `(params: { availableWidth: number; documentWidth: number; fitZoom: number; }) => void` | — | Optional | React when fit-to-width measurements change. | Callback when the implied fit changes (rounded fit zoom or base page width); pixel-level width jitter does not fire it, and `getViewportMetrics()` always reads latest. Registered before the first emit. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onPageMarginsChange` | `(params: { documentId: string; editorVersion: 2; sectionId: string; sectionIndex: number; side: "left" \| "right"; value: number; pageMargins: { top?: number; right?: number; bottom?: number; left?: number; }; }) => void` | — | Optional | React after a ruler drag changes a section margin. | Callback after a ruler drag changes the active section's left or right page margin. | [Ruler](/editor/built-in-ui/ruler) | | `onPageCountKnown` | `(payload: { pageCount: number; generation: number; }) => void` | — | Optional | Read the page count before paint. | Experimental callback fired when paginated layout changes the page count. Runs before paint. `generation` identifies the layout pass. Does not fire in web layout. | [Lifecycle and events](/editor/lifecycle-and-events) | | `onFontsChanged` | `(payload: { source?: "initial" \| "diagnostic-settle" \| "config-change" \| "late-load" \| "render-change"; loadSummary?: null \| FontLoadSummary; report?: FontResolutionRecord[]; missingFonts?: string[]; documentFonts?: string[]; documentFontOptions?: DocumentFontOption[]; }) => void` | — | Optional | Receive final font loading and substitution results. | Called after initial font resolution and whenever substitution or font availability changes. The payload includes the current report, missing fonts, load summary, and the reason for the update. Use `superdoc.fonts.onReport()` for the same subscription at runtime. | [Lifecycle and events](/editor/lifecycle-and-events) | ### Advanced | Field | Type | Default | Status | Summary | API details | Guide | | --- | --- | --- | --- | --- | --- | --- | | `isDev` | `boolean` | — | Optional | Enable development behavior for this instance. | Whether the SuperDoc is in development mode. | — | | `disablePiniaDevtools` | `boolean` | — | Optional | Disable Pinia and Vue devtools for this instance. | Disable Pinia/Vue devtools plugin setup for this SuperDoc instance (useful in non-Vue hosts). | — | | `layoutEngineOptions` | `{ flowMode?: "paginated" \| "semantic"; trackedChanges?: object; virtualization?: { enabled?: boolean; window?: number; overscan?: number; }; showBookmarks?: boolean; showFormattingMarks?: boolean; paintHud?: boolean; }` | — | Optional | Override page layout and rendering behavior. | Layout engine overrides passed through to DocumentRendererRuntime (page size, margins, virtualization, zoom, debug label, etc.). | [Performance](/editor/performance-and-large-documents) | | `isInternal` | `boolean` | — | Optional | Set whether the current user creates and reviews internal comments. | Whether the current user is internal. This affects comment visibility, new-comment metadata, and the default permission decision. It is not an authorization boundary. | — | | `isDebug` | `boolean` | — | Optional | Enable debug behavior. | Whether to enable debug mode. | — | | `workerStartupTimeoutMs` | `number` | `30000` | Optional | Set how long the document worker may take to start. | Budget for the document worker to start up, in milliseconds (default: 30000). Measured from worker spawn, so it covers script download, parsing, evaluation, and the worker's first response to SuperDoc. Raise it when a large worker chunk is served over a slow connection or a cold dev-server cache; lower it to fail faster. Worker load errors are reported immediately and do not wait for this budget. Must be a finite positive number no greater than 2147483647, the platform timer ceiling above which a delay would fire immediately. | [Performance](/editor/performance-and-large-documents) | | `useLayoutEngine` | `boolean` | — | Optional | Pass or omit layout engine options when a DOCX editor opens. | Whether `layoutEngineOptions` are passed when a DOCX editor opens. Set to `false` to omit `layoutEngineOptions` and use CSS fallback styling for the initial non-default zoom. This does not select a different DOCX renderer. `viewOptions.layout` separately selects print or web layout. | — | ## Update a running Editor [#update-a-running-editor] Configuration sets the starting state. After `onReady`, use a runtime method when a value needs to change. | Change | Runtime method | | ------------ | ------------------------------ | | Open a DOCX | `replaceFile()` | | Switch modes | `setDocumentMode()` | | Change zoom | `setZoom()` or `setZoomMode()` | Changing the object passed to `new SuperDoc()` does not update a running Vanilla instance. In React, `documentMode` updates as a prop. Access other runtime methods through `editorRef.current?.getInstance()`. If a Vanilla setting has no runtime method, call `destroy()` on the current instance before creating its replacement. In React, remount `SuperDocEditor` by changing its `key`; the wrapper destroys the old instance. ## Continue to document modes [#continue-to-document-modes] [Compare editing, suggesting, and viewing](/editor/document-modes), then choose what viewers see when a document has comments or tracked changes. --- # Open dialogs and floating surfaces > Open temporary application UI over the Editor and handle every way it can finish. Use a surface when your application needs temporary UI over the Editor. Choose a dialog when the user must respond before returning to the document. Choose a floating surface when the document should remain usable. ## Try both surface modes [#try-both-surface-modes] 1. Open **confirmation**. Continue submits it. Cancel, Escape, or the backdrop closes it. 2. Open **inspector** and keep editing the document. Open it again to replace the first inspector. > **Live example: compare a dialog and a floating surface** > > Open confirmation to see a modal dialog finish as `submitted` or `closed`. Open inspector twice to see the first floating surface finish as `replaced` while the new inspector stays open. The floating inspector leaves the document usable. The status line reports each lifecycle result. The status line reports `submitted`, `closed`, or `replaced`. Destroying the Editor produces the remaining outcome, `destroyed`. ## Choose the right positioning API [#choose-the-right-positioning-api] | Need | API | | ----------------------------- | ----------------------------------------------------------------------- | | A modal decision | `openSurface({ mode: 'dialog' })` | | A fixed, non-modal helper | `openSurface({ mode: 'floating' })` | | UI that follows selected text | [Selection and viewport APIs](/editor/custom-ui/selection-and-viewport) | A floating surface stays in one of the Editor viewport's placement slots. It does not follow a document range. Use the selection and viewport APIs for a selection toolbar, AI prompt, or other text-anchored UI. ## Open a dialog [#open-a-dialog] Start from the [quickstart](/editor/quickstart) with `/sample.docx` in your application's `public` directory. This example mounts plain DOM in the surface, but `render()` can mount your framework instead. ```ts import { SuperDoc, type SurfaceOutcome } from 'superdoc'; import 'superdoc/style.css'; const superdoc = new SuperDoc({ selector: '#editor', document: '/sample.docx' }); type ConfirmationResult = Readonly<{ action: 'continue' }>; export const confirmInEditor = async (message: string): Promise => { const handle = superdoc.openSurface({ mode: 'dialog', title: 'Confirm action', render: ({ container, close, resolve }) => { const text = document.createElement('p'); text.textContent = message; const cancel = document.createElement('button'); cancel.type = 'button'; cancel.textContent = 'Cancel'; const confirm = document.createElement('button'); confirm.type = 'button'; confirm.textContent = 'Continue'; const cancelAction = () => close('cancel'); const confirmAction = () => resolve({ action: 'continue' }); cancel.addEventListener('click', cancelAction); confirm.addEventListener('click', confirmAction); container.append(text, cancel, confirm); return { destroy() { cancel.removeEventListener('click', cancelAction); confirm.removeEventListener('click', confirmAction); }, }; }, }); const outcome: SurfaceOutcome = await handle.result; switch (outcome.status) { case 'submitted': return outcome.data?.action === 'continue'; case 'closed': return false; case 'replaced': return false; case 'destroyed': return false; } }; window.addEventListener('beforeunload', () => superdoc.destroy()); ``` SuperDoc owns the dialog shell, position, and focus behavior. Your `render()` callback owns the content. It receives an empty `container` plus `resolve()` and `close()`. Return `{ destroy() {} }` to remove listeners or unmount a framework root when the surface closes. ## Handle every outcome [#handle-every-outcome] `handle.result` resolves once. `resolve(data)` produces `submitted`. `close(reason)`, Escape, and backdrop clicks produce `closed`. Opening another surface in the same mode produces `replaced` for the previous handle. Destroying the Editor produces `destroyed`. Dialog and floating modes have separate active slots, so one of each can be open. Opening a second surface in either mode replaces only the surface in that mode. Invalid requests throw synchronously. Normal lifecycle outcomes resolve `handle.result`; they do not reject it. ## Use the interaction defaults deliberately [#use-the-interaction-defaults-deliberately] Dialogs trap focus, restore it when they close, and close on Escape or backdrop clicks by default. Floating surfaces default to the top-right, focus their first control, close on Escape, and stay open after outside pointer events. The dialog's trap covers keystrokes inside its own backdrop. Controls your application renders outside the Editor viewport are not part of it, so moving focus to one takes the reader out of the dialog and Escape stops closing it. While a dialog is open, make the rest of your interface unreachable — `inert` on the surrounding region is enough — and do not rely on the trap alone to contain focus. The trap also needs something to hold. It cycles between the first and last enabled focusable elements inside the backdrop, so a dialog whose `render()` adds none — text-only content, or controls that are all disabled — has no cycle to enforce and Tab leaves it on the first press. The shell itself takes initial focus but does not retain it. Give every dialog at least one enabled control, such as a confirm or close button, even when its content is only a message. Give every surface a visible `title`, `ariaLabel`, or `ariaLabelledBy`. Override the close and focus defaults only when the workflow needs different behavior. These options control interaction, not authorization or transaction safety. Configure `surfaces` for shared defaults or intent resolution. Direct `openSurface()` calls do not need a resolver. ## Verify the lifecycle [#verify-the-lifecycle] Open a confirmation and complete it each way. Open the inspector twice and confirm the first handle reports `replaced`. Confirm focus returns to the control that opened a dialog. Next, [apply your product theme to the Editor UI](/editor/theming). --- # Choose a document mode > Choose how the Editor handles edits and what appears in viewing mode. The [Configuration](/editor/configuration) guide starts `/sample.docx` in `suggesting` mode. Compare it with `editing` and `viewing`, then choose the behavior your application needs. ## Try each mode [#try-each-mode] Expand the Editor. Try the replacement in Editing, then reset the sample. Switch to Suggesting and replace `30 days` with `60 days`. Switch to Viewing, then use Changes to compare Original, Markup, and Final against that same proposal. > **Interactive editor: Try document modes** > > Sample: [open the fixture](/fixtures/document-modes.docx). > > Preset: `document-modes`. > > Try the same edit in each mode. Editing changes the document directly and is the default. Suggesting records a tracked change. After making a suggestion, switch to Viewing and use Changes to choose Original, Markup, or Final for the same proposal. > > Local DOCX selection: disabled. | Mode | What happens | Use it when | | ------------ | ---------------------------------- | ------------------------------------------------- | | `editing` | The text changes directly. | Changes should become part of the document. | | `suggesting` | The edit becomes a tracked change. | Another person should review the proposed change. | | `viewing` | Editing is disabled. | A person should read without changing the DOCX. | `editing` is the default. Modes change Editor behavior in the browser. They do not decide who can open or save the document. ## Apply the mode to your project [#apply-the-mode-to-your-project] Set `documentMode` when the Editor starts. These examples start in `suggesting`, configure the later viewing state, and show how your application can switch to `viewing`. **Vanilla — `src/main.ts`** ```ts import { SuperDoc } from 'superdoc'; import 'superdoc/style.css'; let ready = false; const superdoc = new SuperDoc({ selector: '#editor', document: '/sample.docx', documentMode: 'suggesting', viewing: { comments: true, trackedChanges: 'markup', }, onReady: () => { ready = true; }, }); export function switchToViewing() { if (!ready) return; superdoc.setDocumentMode('viewing'); } window.addEventListener('beforeunload', () => superdoc.destroy()); ``` **React — `src/Editor.tsx`** ```tsx import { useState } from 'react'; import { SuperDocEditor, type DocumentMode } from '@superdoc/react'; import '@superdoc/react/style.css'; export function Editor() { const [documentMode, setDocumentMode] = useState('suggesting'); return ( <> ); } ``` Vanilla calls `setDocumentMode()` on the ready Editor. React updates the `documentMode` prop. Neither change remounts the Editor. ## Choose how tracked changes appear [#choose-how-tracked-changes-appear] Viewing stays read-only. Its `trackedChanges` option changes how proposals appear without accepting or rejecting them. For the `30 days` to `60 days` proposal in the demo: | `trackedChanges` | What appears in viewing mode | Use it to | | ---------------- | ------------------------------------------------------ | -------------------------------------------- | | `original` | `30 days`, without change marks. | Show the document before the proposal. | | `markup` | `30 days` deleted and `60 days` inserted, both marked. | Show exactly what the proposal changes. | | `final` | `60 days`, without change marks. | Preview the document as if it were accepted. | `original` is the default. These options only change the display. The proposal remains in the DOCX. Set `viewing.comments` to `true` to show comment anchors and threads. Comments are hidden by default. To change a mounted viewer, call `superdoc.setViewingOptions({ trackedChanges: 'final' })` after `onReady`. Omitted options keep their current values. For review controls, see [Track changes](/editor/track-changes). For comment threads, see [Comments](/editor/built-in-ui/comments). ## Continue to load and save [#continue-to-load-and-save] [Load and save a DOCX](/editor/load-and-save-documents) to connect the same project to your storage. --- # Control DOCX export > Download a DOCX, return its bytes, remove comments, or package related files. Use `SuperDoc.export()` after the Editor is ready. It downloads a DOCX by default; set options when the application needs a backend upload, a comment-free copy, or a package of related files. ## Choose the output behavior [#choose-the-output-behavior] | Option | Default | Use it for | | ---------------------- | ------------ | --------------------------------------------------------------------- | | `exportType` | `['docx']` | Select the DOCX output for the browser Editor workflow | | `exportedName` | Editor title | Set the downloaded filename without its extension | | `triggerDownload` | `true` | Start a browser download; set it to `false` to return a `Blob` or ZIP | | `commentsType` | `'external'` | Keep external comments or use `'clean'` to remove comments | | `isFinalDoc` | `false` | Reserved in the export options; the v2 Editor does not apply it | | `additionalFiles` | `[]` | Add related `Blob` objects and return or download a ZIP | | `fieldsHighlightColor` | `null` | Set a field highlight color in the exported DOCX | ## Export without comments [#export-without-comments] This example removes comments and returns bytes for application-owned storage: ```ts const result = await superdoc.export({ exportType: ['docx'], commentsType: 'clean', exportedName: 'contract-without-comments', triggerDownload: false, }); if (!(result instanceof Blob)) { throw new Error('SuperDoc did not return a DOCX blob.'); } await saveDocument(result); ``` The v2 Editor does not apply `isFinalDoc` during export. `commentsType: 'clean'` removes comments, but it does not accept or reject tracked changes. Resolve tracked changes through the [Editor review workflow](/editor/track-changes) before exporting when the destination requires final text. ## Package related files [#package-related-files] Pair each additional `Blob` with a filename. When export produces more than one file, SuperDoc returns or downloads a ZIP: ```ts const bundle = await superdoc.export({ exportType: ['docx'], additionalFiles: [auditLog], additionalFileNames: ['audit.json'], exportedName: 'contract-package', triggerDownload: false, }); ``` Keep the arrays in the same order. SuperDoc creates the package; your application still owns access control, retention, and persistence. > **Verification target (success)** > > Reopen the exported DOCX. Confirm that the expected text is present and comments follow the selected policy. For the basic download and backend upload paths, see [Load and save documents](/editor/load-and-save-documents). --- # SuperDoc Editor > Understand how SuperDoc edits DOCX files before you add the Editor to your application. SuperDoc adds DOCX editing to your web application. People work in a document editor while your application controls where files come from, where they are saved, and which workflows surround them. ## Keep DOCX as the document [#keep-docx-as-the-document] A DOCX is a package of XML parts, relationships, and assets. An HTML-first editor converts that package into another format for editing, then reconstructs a DOCX for export. SuperDoc takes a different path. It reads the document's OOXML package into editable state and writes changes back to OOXML without converting the document to HTML. The browser DOM is a rendered view, not the document format. | Approach | Editing model | DOCX output | | ----------------- | --------------------------- | ----------------------------------- | | HTML-first editor | Converted HTML | Reconstructed from HTML | | SuperDoc | OOXML-backed document state | Changes written back to the package | ## Fit the Editor into your application [#fit-the-editor-into-your-application] The browser Editor mounts inside your application and needs no SuperDoc server to render or edit a document. Your application chooses the document source, storage, authentication, and collaboration provider. People edit through the browser UI. Application code can use the Document API against the same open document. For server-side automation and agent workflows, SuperDoc provides headless tools built on the same OOXML-backed model. SuperDoc is open source under AGPLv3. A commercial license is available for proprietary applications. See [Licensing](/resources/license) for the requirements of each option. ## What changed in v2? [#what-changed-in-v2] SuperDoc v1 used ProseMirror as its authoritative browser editing model. V2 uses an OOXML-backed document model across browser and headless workflows. If you already use v1, start with [Migrate from v1](/editor/migrate-from-v1/overview). ## Open your first document [#open-your-first-document] [Follow the Quickstart](/editor/quickstart) to install the Editor, open a DOCX, make an edit, and export the result. --- # Configure the Editor license > Set the browser Editor license identity. Set the license key when you create the Editor. When telemetry is enabled, the browser Editor sends it with document-open events. It is client-visible configuration, not a secret or an authorization credential. ```ts const superdoc = new SuperDoc({ selector: '#editor', document: '/contract.docx', licenseKey: import.meta.env.VITE_SUPERDOC_LICENSE_KEY, }); ``` When telemetry is enabled and you omit the key, document-open events use the community and evaluation identity. Browser build variables remain visible to the person running the application. Keep document access, collaboration credentials, and service authorization separate from the license identity. This page covers Editor configuration. See [Licensing](/resources/license) for the open-source and commercial terms and the licensing contact. Continue with [Telemetry](/editor/telemetry) to make the Editor's network behavior explicit. --- # Handle lifecycle and events > Show loading and error states, track unsaved edits, save, and clean up the Editor. Connect each Editor signal to one application state: loading, ready, unsaved, saved, or unmounted. ## Follow the lifecycle [#follow-the-lifecycle] Select a stage. The preview shows what your application should do and the code that drives it. > **Interactive model: the Editor lifecycle in your application** > > The preview moves `/sample.docx` through the application states that matter to a user. > > 1. **Mount — `new SuperDoc()`:** Show a loading state. Keep document actions disabled while the DOCX opens. > 2. **Ready — `onReady`:** Enable document actions. The document is available. Enable Save or Export and run document queries. > 3. **Edit — `onEditorUpdate`:** Mark the document unsaved. Update your dirty state after an edit. Debounce autosave work if you start it here. > 4. **Save — `export() + fetch()`:** Wait for storage. Export produces DOCX bytes. Mark the document saved only after your backend accepts them. > 5. **Unmount — `destroy()`:** Release the Editor. Call destroy() when the route or component that owns the Editor unmounts. > > **Load fails — `onContentError / onException`:** Show a useful error. Keep document actions disabled. Show a retry path instead of an empty mount point. ## Wire the same states [#wire-the-same-states] Replace the controls and mount point from the [Editor quickstart](/editor/quickstart): ```html Opening…
``` Then connect them to `/sample.docx`. Replace `/api/documents/42` with your save endpoint: ```ts import { SuperDoc } from 'superdoc'; import 'superdoc/style.css'; function requireElement(selector: string) { const element = document.querySelector(selector); if (!element) throw new Error(`${selector} not found.`); return element; } const status = requireElement('#editor-status'); const saveButton = requireElement('#save-docx'); let isReady = false; let editRevision = 0; function showLoadError(error: unknown) { console.error('SuperDoc error', error); if (isReady) return; status.value = 'Could not open the document'; saveButton.disabled = true; } const superdoc = new SuperDoc({ selector: '#editor', document: '/sample.docx', onReady: () => { isReady = true; status.value = 'Ready'; saveButton.disabled = false; }, onEditorUpdate: () => { editRevision += 1; status.value = 'Unsaved changes'; }, onContentError: ({ error }) => showLoadError(error), onException: ({ error }) => showLoadError(error), }); async function saveDocument() { if (!isReady) return; const savedRevision = editRevision; saveButton.disabled = true; status.value = 'Saving…'; try { const file = await superdoc.export({ exportType: ['docx'], triggerDownload: false, }); if (!(file instanceof Blob)) throw new Error('Expected one DOCX file.'); const response = await fetch('/api/documents/42', { method: 'PUT', headers: { 'content-type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }, body: file, }); if (!response.ok) throw new Error(`Save failed with ${response.status}.`); status.value = editRevision === savedRevision ? 'Saved' : 'Unsaved changes'; } catch (error) { status.value = 'Save failed'; console.error('The document was not saved.', error); } finally { saveButton.disabled = false; } } saveButton.addEventListener('click', saveDocument); export function unmountEditor() { saveButton.removeEventListener('click', saveDocument); superdoc.destroy(); } ``` Call `unmountEditor()` from the route or component that owns the Editor. If a listener belongs to a temporary panel, register it with `on()` when the panel opens and remove it with `off()` when the panel closes. ## Check the flow [#check-the-flow] With your save endpoint running, reload the page. **Save DOCX** should stay disabled until the document opens. Make an edit, then save. The status should change from **Unsaved changes** to **Saved** only after the request succeeds. Temporarily change `/sample.docx` to a missing URL. The page should show a load error and keep **Save DOCX** disabled. ## Go deeper [#go-deeper] * [Load and save documents](/editor/load-and-save-documents) connects the Editor to your backend storage. * [Connect two editors](/editor/collaboration/connect-two-editors) adds the separate collaboration-ready boundary and connection cleanup. * [Tune performance for large documents](/editor/performance-and-large-documents) shows how to react to pagination without treating layout as an edit. --- # Load and save a DOCX > Load a DOCX from your application and save the edited file back to your backend. Continue with `/sample.docx` from the [Quickstart](/editor/quickstart). Replace the local file with a backend round trip: 1. Fetch the DOCX from your API as a `Blob`. 2. Open and edit the `Blob` in SuperDoc. 3. Export the edited DOCX as a new `Blob` and send it back to your API. SuperDoc handles the DOCX in the browser. Your application owns the API and storage. ## 1. Add a document endpoint [#1-add-a-document-endpoint] The examples use `/api/documents/sample`. This is an application route, not a SuperDoc route. | Request | Your endpoint must | | ------- | --------------------------------------------------------------------------- | | `GET` | Return the current DOCX bytes | | `PUT` | Store the request body and return a success status after the write finishes | The Quickstart Vite project does not create this endpoint. Connect the example to your application API, or add a local endpoint with this contract. The endpoint can read from a database or an object store such as Amazon S3. The browser code stays the same. Keep storage credentials and access checks in your backend. ## 2. Add a save action [#2-add-a-save-action] For Vanilla, replace the Quickstart controls with a save action and status: ```html Opening…
``` The React example below renders its own controls. ## 3. Load and save the document [#3-load-and-save-the-document] The examples show the complete storage flow. Add any user, mode, or viewing options you chose on the previous pages to the Editor configuration. **Vanilla — `src/main.ts`** ```ts import { DOCX, SuperDoc } from 'superdoc'; import 'superdoc/style.css'; function requireElement(selector: string) { const element = document.querySelector(selector); if (!element) throw new Error(`${selector} not found.`); return element; } const saveButton = requireElement('#save-docx'); const status = requireElement('#document-status'); const endpoint = '/api/documents/sample'; let isReady = false; let editRevision = 0; let superdoc: SuperDoc | undefined; function showOpenError(error: unknown) { console.error('Could not open the document.', error); if (isReady) return; status.value = 'Could not open the document. Reload to try again.'; saveButton.disabled = true; } try { const response = await fetch(endpoint); if (!response.ok) throw new Error(`Could not load the document: ${response.status}`); const docx = new Blob([await response.arrayBuffer()], { type: DOCX }); superdoc = new SuperDoc({ selector: '#editor', document: docx, onReady: () => { isReady = true; status.value = 'Ready'; saveButton.disabled = false; }, onEditorUpdate: () => { editRevision += 1; status.value = 'Unsaved changes'; }, onContentError: ({ error }) => showOpenError(error), onException: ({ error }) => showOpenError(error), }); } catch (error) { showOpenError(error); } saveButton.addEventListener('click', async () => { if (!superdoc || !isReady) return; const savedRevision = editRevision; saveButton.disabled = true; status.value = 'Saving…'; try { const editedDocx = await superdoc.export({ exportType: ['docx'], triggerDownload: false, }); if (!(editedDocx instanceof Blob)) throw new Error('Expected one DOCX file.'); const saveResponse = await fetch(endpoint, { method: 'PUT', headers: { 'content-type': DOCX }, body: editedDocx, }); if (!saveResponse.ok) throw new Error(`Could not save the document: ${saveResponse.status}`); status.value = editRevision === savedRevision ? 'Saved' : 'Unsaved changes'; } catch (error) { status.value = 'Save failed. Try again.'; console.error('The document was not saved.', error); } finally { saveButton.disabled = false; } }); window.addEventListener('beforeunload', () => superdoc?.destroy()); ``` **React — `src/App.tsx`** ```tsx import { useEffect, useRef, useState } from 'react'; import { SuperDocEditor, type SuperDocRef } from '@superdoc/react'; import '@superdoc/react/style.css'; const endpoint = '/api/documents/sample'; const docxType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; export default function App() { const editorRef = useRef(null); const editRevisionRef = useRef(0); const savingRef = useRef(false); const [document, setDocument] = useState(); const [loadError, setLoadError] = useState(false); const [ready, setReady] = useState(false); const [saving, setSaving] = useState(false); const [saveStatus, setSaveStatus] = useState(''); useEffect(() => { const controller = new AbortController(); void fetch(endpoint, { signal: controller.signal }) .then(async (response) => { if (!response.ok) throw new Error(`Could not load the document: ${response.status}`); setDocument(new Blob([await response.arrayBuffer()], { type: docxType })); }) .catch((error: unknown) => { if (!controller.signal.aborted) { setLoadError(true); console.error(error); } }); return () => controller.abort(); }, []); function showOpenError(error: unknown) { console.error('Could not open the document.', error); setReady(false); setLoadError(true); } async function saveDocument() { if (savingRef.current) return; const superdoc = editorRef.current?.getInstance(); if (!superdoc) return; const savedRevision = editRevisionRef.current; savingRef.current = true; setSaving(true); setSaveStatus('Saving…'); try { const editedDocx = await superdoc.export({ exportType: ['docx'], triggerDownload: false, }); if (!(editedDocx instanceof Blob)) throw new Error('Expected one DOCX file.'); const response = await fetch(endpoint, { method: 'PUT', headers: { 'content-type': docxType }, body: editedDocx, }); if (!response.ok) throw new Error(`Could not save the document: ${response.status}`); setSaveStatus(editRevisionRef.current === savedRevision ? 'Saved' : 'Unsaved changes'); } catch (error) { setSaveStatus('Save failed. Try again.'); console.error('The document was not saved.', error); } finally { savingRef.current = false; setSaving(false); } } if (loadError) return

Could not open the document. Reload to try again.

; if (!document) return

Opening document…

; return ( <> {saveStatus} showOpenError(error)} onEditorUpdate={() => { editRevisionRef.current += 1; setSaveStatus('Unsaved changes'); }} onException={({ error }) => showOpenError(error)} onReady={() => setReady(true)} ref={editorRef} /> ); } ``` Both examples: * fetch the DOCX before mounting the Editor; * enable saving after `onReady`; * call `export({ triggerDownload: false })` to receive the edited `Blob`; * show **Saved** only after the `PUT` succeeds. Edits made during that request remain unsaved. ## Use another source or destination [#use-another-source-or-destination] | Task | Use | | ------------------------------ | ---------------------------------------------------------- | | Open a public or signed URL | `document: url` | | Open a file selected by a user | `document: file` | | Switch the mounted Editor | `await superdoc.replaceFile(nextDocx)` | | Download instead of saving | `await superdoc.export({ exportedName: 'sample-edited' })` | A URL from another origin must allow the browser request through CORS. Fetch the file yourself and pass a `Blob` when the request needs custom headers. ## Verify the round trip [#verify-the-round-trip] Change the effective date from `September 1, 2026` to `October 1, 2026`. Select **Save DOCX**, then reload the page. The new date should still be present. If the `PUT` fails, the status should show **Save failed** and the stored file should remain unchanged. ## Continue from here [#continue-from-here] * [Version history](/editor/version-history) keeps earlier saves so users can restore them. * [Export options](/editor/export-options) changes the filename, comment policy, or related files. * If your content starts as HTML or Markdown, see [rich content](/document-api/rich-content) and [output projections](/document-api/output-projections). These APIs do not open either format as a DOCX file. --- # Tune performance for large documents > Keep page rendering responsive, choose a layout, and measure representative DOCX workflows. SuperDoc virtualizes paginated documents by default. Keep that baseline until measurements from representative DOCX files show a specific scrolling, memory, loading, or worker-startup problem. ## Tune the page window deliberately [#tune-the-page-window-deliberately] The default paginated window is five pages with one overscan page. Set it explicitly only when comparing a measured alternative: ```ts const superdoc = new SuperDoc({ selector: '#editor', document: '/large-contract.docx', layoutEngineOptions: { virtualization: { enabled: true, window: 5, overscan: 1, }, }, onPaginationUpdate: ({ totalPages }) => { pageCount.textContent = String(totalPages); }, }); ``` A larger window can reduce repaints during fast scrolling while keeping more pages mounted. A smaller window reduces mounted work but can make navigation less smooth. Change one value at a time and measure on the devices and documents the product supports. ## Choose print or web layout [#choose-print-or-web-layout] Paginated print layout preserves page boundaries and uses page virtualization. Web layout reflows semantic content to the container and does not provide headers, footers, rulers, or page-count updates. Use `viewOptions: { layout: 'web' }` with `layoutEngineOptions: { flowMode: 'semantic' }` for the continuous surface. [Build a responsive Editor layout](/editor/built-in-ui/responsive-layout) compares the two layouts and their host sizing requirements. ## Control worker startup [#control-worker-startup] Keep the default 30-second `workerStartupTimeoutMs` unless measurements show that the worker bundle needs more time to download or evaluate. Use `workerUrls` when the application must serve the document, collaboration, and review-index module workers from explicit same-origin URLs. Worker load errors fail immediately; increasing the timeout does not repair a missing file, blocked Content Security Policy, or invalid worker response. See [Secure integration](/editor/secure-integration) for the browser boundary. ## Measure the whole task [#measure-the-whole-task] Test opening, first interaction, scrolling, editing, collaboration, and export with representative DOCX files. Record document size and page count with the result so a regression can be reproduced. > **Verification target (success)** > > On the slowest supported device, open a representative large DOCX, scroll from start to end, edit text on distant > pages, and export. No action should lose document state or leave controls permanently busy. --- # Open and edit your first DOCX > Install the browser Editor, open a sample DOCX, and export your first edit. Choose Vanilla or React below. Your choice stays active for every code example on this page. ## 1. Create a project [#1-create-a-project] Create a Vite project, enter it, and install SuperDoc: **Vanilla — `Terminal`** ```sh pnpm create vite@latest superdoc-quickstart --template vanilla-ts cd superdoc-quickstart pnpm add superdoc ``` **React — `Terminal`** ```sh pnpm create vite@latest superdoc-quickstart --template react-ts cd superdoc-quickstart pnpm add @superdoc/react ``` ## 2. Add the sample document [#2-add-the-sample-document] Download the sample and save it as `public/sample.docx`: [Download the sample document](/fixtures/getting-started.docx): One-page statement of work · DOCX Vite serves `public/sample.docx` at `/sample.docx`. ## 3. Add the page [#3-add-the-page] Add the application surface. Vanilla mounts SuperDoc into `#editor`; the React wrapper creates that container for you. **Vanilla — `index.html`** ```html SuperDoc vanilla quickstart
``` **React — `src/main.tsx`** ```tsx import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; import './index.css'; const root = document.querySelector('#root'); if (!root) throw new Error('The React root is missing.'); createRoot(root).render( , ); ``` ## 4. Open the document [#4-open-the-document] Add the Editor and enable export after the document opens: **Vanilla — `src/main.ts`** ```ts import { SuperDoc } from 'superdoc'; import 'superdoc/style.css'; const exportButton = document.querySelector('#export-docx'); if (!exportButton) throw new Error('The export button is missing.'); const superdoc = new SuperDoc({ selector: '#editor', document: '/sample.docx', onReady: () => { exportButton.disabled = false; }, onContentError: ({ error }) => { console.error('SuperDoc could not open the document.', error); }, onException: ({ error }) => { console.error('SuperDoc could not open the document.', error); }, }); exportButton.addEventListener('click', async () => { exportButton.disabled = true; try { await superdoc.export({ exportType: ['docx'], exportedName: 'sample-edited' }); } catch (error) { console.error('SuperDoc could not export the document.', error); } finally { exportButton.disabled = false; } }); ``` **React — `src/App.tsx`** ```tsx import { useRef, useState } from 'react'; import { SuperDocEditor, type SuperDocRef } from '@superdoc/react'; import '@superdoc/react/style.css'; export default function App() { const editorRef = useRef(null); const exportingRef = useRef(false); const [ready, setReady] = useState(false); const [exporting, setExporting] = useState(false); async function exportDocument() { if (exportingRef.current) return; exportingRef.current = true; setExporting(true); try { await editorRef.current?.getInstance()?.export({ exportType: ['docx'], exportedName: 'sample-edited' }); } catch (error) { console.error('SuperDoc could not export the document.', error); } finally { exportingRef.current = false; setExporting(false); } } return (
console.error('SuperDoc could not open the document.', error)} onException={({ error }) => console.error('SuperDoc could not open the document.', error)} onReady={() => setReady(true)} ref={editorRef} />
); } ``` Run the project: ```bash pnpm dev ``` Open the printed URL. When the statement of work appears, **Export DOCX** becomes enabled. `onReady` keeps the button disabled until the document is open. ## 5. Make an edit and export [#5-make-an-edit-and-export] Change the effective date from `September 1, 2026` to `October 1, 2026`. Then select **Export DOCX**. The browser downloads `sample-edited.docx`. > **Check the exported document (success)** > > Open `sample-edited.docx` in Word or SuperDoc. Confirm that the effective date is `October 1, 2026` and that the > title, service list, milestone table, and signatures keep their formatting. ## What just happened [#what-just-happened] * `selector` mounted the Vanilla Editor; the React wrapper owned its container. * `document` gave the browser a DOCX URL to open. * `onReady` marked the first safe moment to enable document actions. * `export()` created the edited DOCX and triggered the download. If the document does not appear, check the `onContentError` or `onException` message in the browser console. They report import and startup failures instead of leaving an unexplained empty mount point. The complete projects are available for [Vanilla](https://go.superdoc.dev/examples/vanilla) and [React](https://go.superdoc.dev/examples/react). ## Continue to configuration [#continue-to-configuration] [Configure the Editor](/editor/configuration) to add user information, choose a document mode, and handle startup errors while keeping the same `/sample.docx` project. --- # Secure browser document workflows > Define trusted boundaries for document access, identity, persistence, integrations, and browser policy. SuperDoc runs document editing in the browser. That improves data locality, but it does not make every integration private or make client code a trusted authorization boundary. ## Identify every data boundary [#identify-every-data-boundary] * A URL document is fetched by the browser from its origin. * A local `File` or `Blob` stays in browser memory until application code sends or persists it. * Exported bytes remain local unless downloaded, uploaded, cached, or passed to another API. * Collaboration providers transmit document updates and awareness data. * Network proofing, AI, telemetry, logging, and upload handlers may transmit document content or metadata. Publish this behavior in product privacy language. Minimize data, retention, and logs. Never log document bodies, clauses, comment text, credentials, signed URLs, or mutation payloads by default. ## Keep trusted decisions on trusted services [#keep-trusted-decisions-on-trusted-services] Document mode, disabled buttons, permission resolvers, read-only comments, and hidden review controls guide normal browser interaction. They do not enforce access against a user who controls the browser. Authenticate document requests and enforce read, write, export, collaboration, and agent approval policy on trusted services. Treat names and emails supplied in `user` as display identity unless they came from an authenticated application session. Validate uploaded DOCX files, set size and timeout limits, and isolate sensitive processing where your threat model requires it. ## Configure the browser boundary [#configure-the-browser-boundary] Use HTTPS, restrictive CORS, and a Content Security Policy that permits only the scripts, workers, connections, images, and fonts your deployment needs. Avoid unversioned third-party runtime dependencies, and decide whether Editor assets are self-hosted before production. ## Pass a nonce to runtime styles [#pass-a-nonce-to-runtime-styles] Leave `cspNonce` unset unless the page's CSP requires a nonce for `