# DOCX Editor: full documentation This file contains the complete docs for the @docx-editor.dev/* packages, concatenated and stripped of HTML/frontmatter so an LLM can ingest it in one shot. Source of truth: https://www.docx-editor.dev/docs/2.x Generated from: src/content/docs/*.mdx (49 files) --- # Build a DOCX agent Source: https://www.docx-editor.dev/docs/2.x/build-a-docx-agent This page connects a tool-calling model to a DOCX file. You expose a small catalog of document tools. Each tool validates its input and calls [`@docx-editor.dev/editor-api`](/docs/2.x/editor-api). The model does not generate Office Open XML (OOXML). ## Try the writer agent Open the [writer agent demo](/#writer-agent) on the homepage. Choose **Draft a mutual NDA.** After the draft finishes, choose **Redline for clarity**. The demo uses the Vercel AI SDK. The document tools work with any tool-calling framework. For an agent that continues working after the browser closes, run the [server agent review example](https://github.com/eigenpal/docx-editor/tree/main/examples/server-agent-review). Its worker joins a shared Hocuspocus room and publishes suggestions for browser peers to accept or reject. Scripted review works without a model API key. ## Before you start - You need `@docx-editor.dev/editor-api` and `@docx-editor.dev/core`. - If you show the document in a page, install `@docx-editor.dev/react` or `@docx-editor.dev/vue`. - For comments or tracked changes, pass a non-empty `author`. Browser review also requires `@docx-editor.dev/pro` and its review module. The server runtime provides these writes without a browser review module. ### Install the packages Install the editing API and the engine: ```bash npm install @docx-editor.dev/editor-api @docx-editor.dev/core ``` If you mount an editor in the page, also install an adapter: #### React ```bash npm install @docx-editor.dev/react ``` #### Vue ```bash npm install @docx-editor.dev/vue ``` If you display or edit comments and tracked changes in the browser, also install Pro: ```bash npm install @docx-editor.dev/pro ``` ### Create a runtime If you have DOCX bytes and no editor in the page, create a server runtime. The server runtime returns edited bytes. It does not start Microsoft Word and does not use a browser Document Object Model (DOM). ```ts import { DocxEditor } from '@docx-editor.dev/editor-api'; const runtime = await DocxEditor.createServer(bytes, { author: 'Contract agent', }); ``` If a reader already has the document open, create a browser runtime. Edits use the editor undo stack and appear in the page. ```ts import { DocxEditor } from '@docx-editor.dev/editor-api/browser'; const runtime = DocxEditor.createBrowser(editor, { author: 'Review agent', }); ``` Call `runtime.dispose()` when the work ends. For capabilities, `sync()` rules, and disposal, see [Editing API](/docs/2.x/editor-api). ### Define one tool per document task Give the model a small catalog. Each tool does one job, validates its input, and runs inside one `runtime.run` callback. This AI SDK example defines a read tool and two write tools: ```ts import type { DocxEditorRuntime } from '@docx-editor.dev/editor-api'; import { tool } from 'ai'; import { z } from 'zod'; export function createDocumentTools(runtime: DocxEditorRuntime) { return { read_document: tool({ description: 'Read the current document text.', inputSchema: z.object({}), execute: async () => runtime.run(async (context) => { const body = context.document.body; body.load('text'); await context.sync(); return { text: body.text }; }), }), append_paragraph: tool({ description: 'Add one paragraph to the end of the document.', inputSchema: z.object({ text: z.string().min(1), }), execute: async ({ text }) => runtime.run(async (context) => { context.document.body.insertParagraph(text, 'End'); await context.sync(); return { inserted: true }; }), }), replace_exact_text: tool({ description: 'Replace one exact phrase when it occurs once.', inputSchema: z.object({ search: z.string().min(1), replacement: z.string(), }), execute: async ({ search, replacement }) => runtime.run(async (context) => { const matches = context.document.body.search(search, { matchCase: true, }); matches.load('items'); await context.sync(); if (matches.items.length !== 1) { return { replaced: false, reason: `Expected one match, found ${matches.items.length}.`, }; } matches.items[0].insertText(replacement, 'Replace'); await context.sync(); return { replaced: true }; }), }), }; } ``` Return a structured result. If a search matches zero times or more than once, return a refusal. Do not pick a match for the model. This package does not include a model, a tool catalog, or a chat UI. Keep those in your application. ### Address text with an exact phrase Pass a verbatim `search` phrase from the paragraph you read. Copy case and punctuation. If you send a document snapshot with the chat request, cap it by paragraph count and character count. A large document must not grow every later turn. ### Split a fresh draft into stages Do not put the whole document in one `write_document` tool. Split a draft so you can see each failure: 1. Replace the body with styled paragraphs. Return paragraph IDs. 2. Apply native bullets or numbering. 3. Wrap exact placeholders in content controls. 4. Insert a table, then populate its cell paragraphs. 5. Write the header, footer, and page field. Do not put `•` or `1.` in paragraph text. Native list formatting adds those markers. Literal prefixes show up twice in Word. Each later tool uses IDs from the first tool. If a required stage fails, stop. The [writer agent example](https://github.com/eigenpal/docx-editor/tree/main/examples/write-agent) runs this sequence. The [comment agent example](https://github.com/eigenpal/docx-editor/tree/main/examples/agent) reads an open document and adds anchored comments. ### Choose direct edits or tracked changes Read the user request, then pick tools: - If the user says "update", "rewrite", or "fix", call direct replacement, insertion, or deletion. - If the user says "suggest", "review", or "redline", call tracked proposal tools. On a server, set `context.document.changeTrackingMode = 'TrackMineOnly'` and use `range.insertText()` or `range.delete()` to record tracked text edits. See the [server tracking example](/docs/2.x/editor-api#create-tracked-changes-on-a-server) for prerequisites and limits. Leave accept and reject off the agent allowlist. The reader decides in the editor. Cap tracked proposals per turn. Skip headings, placeholders, and sentences that are already clear. Prefer the smallest edit that fixes the problem. For revision markup, see [Tracked changes](/docs/2.x/pro/tracked-changes). For comment threads, see [Comments](/docs/2.x/pro/comments). ### Wrap values in content controls A content control is a Structured Document Tag (SDT). Use one when a value has identity beyond its visible text, such as a party name or an effective date. Pass a paragraph ID, the exact placeholder text, a `tag`, a `title`, and a `subtype`. Search only that paragraph. If the phrase is missing or appears twice, return a refusal. Wrap the placeholder, not the label. Wrap `[Effective Date]`. Do not wrap `Date of Birth`. For field types and template filling, see [Content controls](/docs/2.x/guides/content-controls). ### Trust the tool result For every write, do the following: 1. Read or search for an exact target. 2. If the match is missing or ambiguous, return a refusal. 3. Apply one document transaction. 4. Return a short success object or a typed refusal. 5. Continue only when the result says the write succeeded. If the model emits parallel tool calls, queue the writes on the client. Do not tell the reader that a table, field, or revision exists until its tool succeeds. ## Next steps - [Editing API](/docs/2.x/editor-api): runtimes, `sync()`, and tool integration - [Office.js compatibility](/docs/2.x/editor-api/office-js-api) - [API reference](/docs/2.x/api/editor-api) - [React composition](/docs/2.x/react/composition): mount chrome around the editor --- # Get started with DOCX collaboration Source: https://www.docx-editor.dev/docs/2.x/collaboration This quickstart uses `useWebrtcCollaboration` to add real-time DOCX collaboration to a React or Vue editor. It connects two editors through WebRTC and shows each participant's changes. Start with a working editor from the [Quickstart](/docs/2.x/quickstart). For provider options and failure states, see the [collaboration reference](/docs/2.x/pro/collaboration). ## Install the packages Install the Pro package, Yjs, and the WebRTC provider: ```bash npm install @docx-editor.dev/pro yjs y-webrtc ``` You also need `@docx-editor.dev/react` or `@docx-editor.dev/vue` in your application. ## Create a room ID Create one room ID and send it to each participant: ```ts import { createCollaborationRoomId } from '@docx-editor.dev/pro/collaboration/webrtc'; const roomId = createCollaborationRoomId(); ``` Store the room ID in your application state or URL. Do not create a new ID during each render. ## Add a collaborative editor Call `useWebrtcCollaboration` with a room ID, an identity, and the initial DOCX bytes. The first editor creates the room. Later editors join its document. ```tsx import { DocxEditor } from '@docx-editor.dev/react'; import { useWebrtcCollaboration } from '@docx-editor.dev/pro/react/webrtc'; interface CollaborativeEditorProps { roomId: string; bytes: Uint8Array; actorId: string; name: string; } export function CollaborativeEditor({ roomId, bytes, actorId, name }: CollaborativeEditorProps) { const { document, modules, session, pending, error } = useWebrtcCollaboration({ room: { roomId, identity: { actorId, name }, bootstrap: { kind: 'create-or-join', document: bytes }, }, }); if (error) { return

{error.detail ?? error.code}

; } if (pending || !document) { return

Connecting…

; } return ; } ``` Vue provides the composable at `@docx-editor.dev/pro/vue/webrtc`. Select Vue in the [collaboration API reference](/docs/2.x/pro/collaboration#connect-with-usewebrtccollaboration) for a complete component. ## Test the room 1. Open the application in one browser tab. 2. Open a second tab with the same `roomId` and a different `actorId`. 3. Type in either editor. Open the first tab before the second tab. This sequence prevents both peers from seeding an empty room at the same time. The example uses the public demo signaling service when you omit `signaling`. Configure your own signaling URLs and TURN servers for production. ## Continue the setup - [Production WebRTC setup](/docs/2.x/pro/collaboration#use-your-own-signaling): configure signaling and TURN servers. - [Hocuspocus setup](/docs/2.x/pro/collaboration#connect-to-a-hocuspocus-server): connect authentication and persistence. - [Custom Yjs providers](/docs/2.x/pro/collaboration#integrate-a-custom-yjs-provider): connect an existing Yjs deployment. - [Tracked changes](/docs/2.x/pro/tracked-changes): suggesting mode and the review sidebar. - [Build a DOCX agent](/docs/2.x/build-a-docx-agent): connect a headless replica to a room for real-time agent edits. --- # Architecture Source: https://www.docx-editor.dev/docs/2.x/core/architecture There is one document model and one pipeline: ```text bytes → bounded OPC/XML read → canonical OOXML tree → layout → painted pages → serialize ``` What you see painted and what an edit mutates are the same tree, so there is no separate view model to synchronize. ## Architecture boundaries | Boundary | Owns | Must not own | | ----------------- | ----------------------------------------------------------------------- | -------------------------------- | | `contracts` | Public engine types and interfaces | Runtime implementation or state | | `store` | Canonical trees, package reads and writes, indexes, and transactions | DOM or ProseMirror state | | `layout` | DOM-free pagination and interaction geometry | Browser layout or authored state | | `output` | Painted pages from layout records | Document mutation | | `binding` | The ProseMirror projection and conversion of edits into tree operations | Canonical document authority | | `automation` | Transport-neutral host operations for browser and server automation | DOM or editor chrome | | `editor` | The public editor facade, surface input, caret, and chrome registry | A second document model | | `react` and `vue` | Providers, hooks or composables, and framework chrome | Editing state | The package boundary keeps one copy of `@docx-editor.dev/core` in an application. Both adapters use it as a peer dependency. ## The canonical tree Reading a `.docx` produces one tree per XML part, up to a bounded part count. Non-XML parts stay as bytes and are never parsed. Nodes are **typed** where layout needs them (the paragraph, run, and table vocabulary) and **generic** everywhere else, preserving the element verbatim. Two consequences: - Content the engine does not model is carried, not dropped. A known element in an invalid position demotes to generic rather than erroring. - Unknown content never blocks editing. A document full of extensions the engine does not recognize opens, accepts edits, and saves. Every write is a transaction over the tree. Content edits are addressed by node id and character offset; package-level operations such as creating a header or setting section properties are addressed by section index. That is the only write path. There is no second way to mutate a document, so undo, the editing API, and every command use the same write path. ## Layout The layout pass is DOM-free. It takes the tree and a text measurer and returns positioned pages, working in the document's own units (twips, half-points, EMUs) rather than approximating from browser layout. Because it is not constrained by what `contenteditable` can express, it lays text out according to Word layout rules: - Pagination is computed, not approximated: lines are measured, blocks split where Word splits them, and tables fragment across page boundaries with Word-compatible border treatment at the split. - Paragraph fidelity resolves through the real style cascade: `w:spacing` line rules, first-line and hanging indents, `w:contextualSpacing`, paragraph borders, tab stops and leaders, list markers from `numbering.xml`, and table styles through their `basedOn` chain gated by `w:tblLook`. - Headers and footers lay out once per variant and attach per page. Editing one uses the same edit path as body content. The pass is incremental: per-block cache keys and flow checkpoints mean a keystroke re-lays out what changed, and a pass with no changes returns the previous pages by identity. ## Paint and interaction The painted pages are `contenteditable`, but the DOM is a non-authoritative rendering. Browser mutations are prevented and re-expressed as tree operations, so the browser cannot insert markup inside your document. The editor also paints its own caret, from layout geometry rather than from the DOM, which is why an empty paragraph gets a caret. On failure it falls back: range selection, IME composition, or a position it cannot place all restore the native caret instead of showing no caret. Selection maps through paragraph identity and offsets. It does not use DOM traversal. | Page furniture | Layout behavior | Interaction behavior | | -------------------------------- | ------------------------------------- | ----------------------------------------- | | Headers and footers | Lay out by section and page variant | Use scoped editing controls | | Page numbers and other furniture | Attach to each painted page | Stay non-editable and outside selection | | Body content | Flows around reserved furniture bands | Uses the normal caret and selection model | ## Saving Every parsed XML part is re-emitted from the canonical tree with structural fidelity, including custom XML and unknown extensions. Package payloads such as embedded fonts, media, and VBA binaries pass through untouched. That is what "structural fidelity" means here: unsupported XML and package payloads survive editing and save without loss. Two oracles gate it in CI: a canonical fingerprint over the tree, and a save-and-reopen semantic digest. ## Adapters `@docx-editor.dev/core` contains the engine and no framework code. An adapter mounts the engine and provides framework-specific state access and chrome. `@docx-editor.dev/react` and `@docx-editor.dev/vue` hold no editing state. Packaged and custom controls consume the same public engine contract. ## Next steps - [Core package overview](/docs/2.x/core): entry points and the `Editor` contract - [Word fidelity](/docs/2.x/word-fidelity): what the fidelity claim covers, and its limits - [Composition](/docs/2.x/react/composition): building on the adapter --- # 2.x/core/index Source: https://www.docx-editor.dev/docs/2.x/core/index This package is the engine. It reads a `.docx` into a canonical document tree, lays that tree out into pages, paints them, and writes the tree back to OOXML. It has no framework dependency and no UI. Most apps never import it directly: `@docx-editor.dev/react` carries it. Import it when you are writing your own adapter, or when you need the contract types to type a function signature. ```bash npm install @docx-editor.dev/core ``` ## Entry points The root is the entry point for typical editor creation and typing: create an editor over bytes, the contract it implements, fonts, and the chrome registry. See the [chrome slot reference](/docs/2.x/guides/chrome-slots) for the complete registry vocabulary and its React and Vue toolbar parts. ```ts import { createDocxEditor, loadFonts, WORD_DEFAULT_FONT } from '@docx-editor.dev/core'; import type { Editor, EditorSnapshot } from '@docx-editor.dev/core'; ``` Import a subpath when you need the canonical tree, the layout pass, or the paint step directly. | Subpath | Contents | | ------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `.` | Create an editor, the `Editor` contract, fonts, the chrome registry, the document model types. | | `./editor` | Everything the root re-exports, plus the paginated surface, ruler geometry, and module resolution. | | `./contracts/editor` | `Editor`, `EditorCommand`, `EditorQuery`, `EditorSnapshot`, `DocumentSource`, `PageSetup`, the contract an adapter renders. | | `./contracts/document` | The document-level edit and query vocabulary. | | `./contracts/types` | Document model types. | | `./contracts/modules` | `EditorModule`, the shape [`@docx-editor.dev/pro`](/docs/2.x/pro) implements. | | `./contracts/interaction` | Semantic addressing (`SemanticTarget`) and the `InteractionOutcome` that an attempt returns. | | `./store` | The canonical tree and its transactional store. | | `./layout` | The DOM-free layout pass. | | `./output` | Paint: turning a laid-out document into page DOM, and the selection overlay over it. | | `./automation` | The document object model behind [`@docx-editor.dev/editor-api`](/docs/2.x/editor-api). | | `./styles/editor.css` | The editor stylesheet. The packaged chrome and your own chrome both use it. | Prefer the root import. Use `./editor` only when you build an adapter and need surface internals (the paginated surface, ruler geometry, module resolution). ## The contract `Editor` is the whole public surface an adapter uses. Call `exec` for commands. Call `query` and `snapshot` for reads: ```ts import type { Editor, EditorSnapshot } from '@docx-editor.dev/core/contracts/editor'; function pageLabel(editor: Editor): string { const snapshot: EditorSnapshot = editor.snapshot(); return `${snapshot.page.current} / ${snapshot.page.total}`; } ``` `snapshot()` is version-cached: it returns the same reference until editor state changes, and its sub-objects are reference-stable. That is what makes `useSyncExternalStore` (and therefore [`useEditorState`](/docs/2.x/react/hooks)) correct without a deep compare on every store notification. ## The pipeline The pipeline has one direction: ```text bytes → bounded OPC/XML read → canonical document tree → layout → painted pages → serialize ``` The painted pages are the editable surface. There is no shadow document model to keep in sync with what you see. Fidelity is structural. The canonical tree preserves XML content and package payloads pass through untouched. Content the engine cannot type (an unknown element, or a known one in an invalid position) becomes a generic node rather than being dropped, so unknown content never blocks editing and never disappears on save. ## Untrusted input A `.docx` is a zip of XML that whoever sent it fully controls. The engine sanitizes at the parse boundary (URL allowlisting, entity and zip-bomb limits, recursion and element caps, no zero-click external fetches, escaping on the way back out) so everything downstream receives an already-sanitized projection. Anything you then render from document data (a font name, a hyperlink target, a comment body, a custom node's attributes) is still attacker-controlled at your boundary. Render it as text; do not build markup or URLs from it. ## Next steps - [Architecture](/docs/2.x/core/architecture): how the pipeline fits together - [React](/docs/2.x/react): the adapter that renders this contract - [Editing API](/docs/2.x/editor-api): the object model over `./automation` --- # 2.x/editor-api/index Source: https://www.docx-editor.dev/docs/2.x/editor-api/index `@docx-editor.dev/editor-api` edits DOCX files through a supported subset of Word's JavaScript object model, including paragraphs, ranges, comments, and revisions. Use `load()` to queue reads and `sync()` to apply each batch atomically. Run the API on a server over DOCX bytes or in the browser against an open editor. See [Office.js compatibility](/docs/2.x/editor-api/office-js-api) for supported members and differences from Word. ```bash npm install @docx-editor.dev/editor-api @docx-editor.dev/core ``` `@docx-editor.dev/core` is a peer dependency: the engine holds identity-keyed state, so your project must resolve exactly one copy of it, shared with any editor adapter you install. ## Choose a host | `DocumentCapabilities` field | Server runtime | Browser runtime | | ---------------------------- | -------------- | --------------- | | `document` | Yes | Yes | | `save` | Yes | No | | `events` | Yes | Yes | | `selection` | No | Yes | | `scrolling` | No | Yes | | `layout` | No | Yes | `runtime.capabilities` reports these values. The values remain fixed for the runtime's lifetime. The current mode and document state can still refuse writes. These dynamic permissions are not capability fields. Use these prerequisites: | Task | Prerequisite | | ------------------------- | ------------------------------------------------------------------------------------------------ | | Create a server runtime | DOCX bytes and one resolved copy of `@docx-editor.dev/core` | | Create a browser runtime | An attached editor from `@docx-editor.dev/react`, `@docx-editor.dev/vue`, or the core editor API | | Write a comment or reply | A non-empty `author` | | Write browser review data | The Pro review module, an editable mode, and a host that accepts the write | | License and support | See the [pricing page](https://www.docx-editor.dev/pricing) | A refusal from `sync()` is authoritative. ## On a server The server runtime is headless. It accepts bytes and returns bytes. ```ts import { readFile, writeFile } from 'node:fs/promises'; import { DocxEditor } from '@docx-editor.dev/editor-api'; const runtime = await DocxEditor.createServer(await readFile('contract.docx'), { author: 'Review bot', }); try { await runtime.run(async (context) => { const matches = context.document.body.search('$50k'); matches.load(); await context.sync(); // one round trip: now you know what was found for (const match of matches.items) match.insertText('$500k', 'Replace'); await context.sync(); // one atomic batch: all of the writes, or none }); await writeFile('contract.filled.docx', await runtime.save()); } finally { runtime.dispose(); } ``` `createServer` completes its bounded parse before it resolves and does not retain the input `Uint8Array`; the caller may reuse or transfer that buffer afterward. Each `save()` returns a fresh, caller-owned `Uint8Array`. Mutating or transferring one save result cannot change the runtime or a later save. The server runtime is detached from every live editor. To load the result into a live editor, make that replacement explicit: ```ts const source = new Uint8Array(await editor.save()); const detached = await DocxEditor.createServer(source); try { // inspect, search, and edit with detached.run(...) const result = await detached.save(); editor.load(result); // the live document changes only here } finally { detached.dispose(); } ``` To discover bookmarks without first knowing their text, enumerate the story that owns them: ```ts await runtime.run(async (context) => { const bookmarks = context.document.body.bookmarks; bookmarks.load('items'); await context.sync(); for (const bookmark of bookmarks.items) bookmark.load('name'); await context.sync(); console.log(bookmarks.items.map(({ name }) => name)); }); ``` `document.body.bookmarks` covers only the main body story. A header or footer `Body` has its own collection; this accessor never combines separate stories into a document-wide answer. ### Handle resource limits `DocxEditor.createServer` rejects with error code `ResourceLimitExceeded` when opening exceeds a resource cap. Catch `DocxEditorError` and inspect its `limit` field, such as `xml.maxElements` or `zip.maxRatio`. The XML reader uses the engine's default budget of 10,000,000 elements per part. Set `limits.xml.maxElements` to lower that budget for untrusted input or raise it for larger documents, up to the engine ceiling of 50,000,000. Set `limits.xml.maxBytes` alongside it. The 64 MiB per-part byte ceiling and 256-level depth ceiling still apply. These budgets do not guarantee that a document fits in the host's memory. Counts and byte budgets must be finite nonnegative integers; compression ratios may be fractional. Malformed input and invalid budget options reject with `InvalidArgument`. ## Create tracked changes on a server Set `context.document.changeTrackingMode = 'TrackMineOnly'` before calling `range.insertText()` or `range.delete()`. Configure an `author` when you create the runtime. The mode applies to that runtime and persists across `run()` calls. Tracked edits support text within one paragraph, including table cells. Edits that touch pending revisions, structural edits, and formatting edits are rejected atomically. `TrackAll` and browser runtime tracking-mode control are unsupported. A pending row insertion or deletion blocks tracked edits throughout that row, including other cells and nested tables. For a complete example, see the [server agent guide](https://github.com/eigenpal/docx-editor/blob/main/packages/editor-api/OFFICE_JS_GUIDE.md). To publish suggestions to a shared document, use [`DocxEditor.createCollaborative`](/docs/2.x/pro/collaboration#let-a-server-agent-propose-redlines). ## In the browser The browser entry takes an editor the host already created (from `@docx-editor.dev/react` or a plain page) and drives it in place. Edits apply to the open document with the reader's undo stack intact, so there is no `save()`: the host continues to call its existing save path. ```ts import { DocxEditor } from '@docx-editor.dev/editor-api/browser'; const runtime = DocxEditor.createBrowser(editor, { author: 'Demo Reviewer' }); await runtime.run(async (context) => { const heading = context.document.body.paragraphs.getFirstOrNullObject(); heading.load('text'); await context.sync(); if (!heading.isNullObject) heading.font.bold = true; await context.sync(); }); ``` The `/browser` entry includes integration with the painted engine. Import the root entry on servers to keep that browser code out of the bundle. The optional `author` has the same meaning as it does for `createServer`. It supplies the identity for comments. Server runtimes also require it for tracked text edits. ## Delete comments and Undo Deleting a top-level `Comment` removes its entire thread and story anchors. Deleting a `CommentReply` removes only that reply and preserves its parent and siblings. Queue multiple deletions before one `sync()` when they should be one atomic browser Undo unit: ```ts const comments = context.document.comments; comments.load('items'); await context.sync(); for (const comment of comments.items.slice(0, 2)) comment.delete(); await context.sync(); // one transaction; editor Undo restores both threads ``` The server runtime supports the same object-model calls without a browser module. ## Programming model - Reading a property you did not `load()` throws. This catches typos before they affect later writes. - Navigation-property expansion is not supported. A non-empty `LoadQueryOptions.expand` is rejected with `InvalidArgument`; load the navigation object or collection explicitly instead. - `sync()` is the only round trip. Everything queued between two syncs is one ordered transaction. If any operation in it is refused, none of them happened. - Objects are proxies into a document the runtime owns. They remain valid across `sync()` calls within one `run`. To carry one into a later `run`, track it and pass it to `runtime.run(object, callback)` for adoption. No proxy remains valid after `dispose()`. - `getFirstOrNullObject` and `getLastOrNullObject` return an object whose `isNullObject` is `true` after the sync, which is the difference between an absent heading and a thrown error. For content-control writes, load `isBound` with the other properties and skip controls where it is true. That boolean is safe preflight metadata, not a write guarantee: `sync()` always checks the current document again and atomically refuses the batch if a control is bound by then. ## Entries | Entry | Use when | | ------------------------------------- | ---------------------------------------------------- | | `@docx-editor.dev/editor-api` | Servers, workers, build scripts: bytes in, bytes out | | `@docx-editor.dev/editor-api/browser` | A page, driving an editor the host already created | Both export the same vocabulary (the lifecycle types, the object model, the error type) so consumer code compiles against either. They differ by one member: `createBrowser`. ## Integrate with an application model This package ships no model integration, tool catalog or chat UI. The application that owns the model defines which operations to expose, how to describe them and how to handle refusals. A tool such as `add_comment` performs its document work inside a `run` block. Keep its chat UI with the application's other chrome. ### Expose focused tools to an agent Give each tool one document task. Validate its input before the tool reaches the document. Keep the complete read or write inside one `run` callback. This AI SDK example exposes one read tool and two small writing tools: ```ts import type { DocxEditorRuntime } from '@docx-editor.dev/editor-api'; import { tool } from 'ai'; import { z } from 'zod'; export function createDocumentTools(runtime: DocxEditorRuntime) { return { read_document: tool({ description: 'Read the current document text.', inputSchema: z.object({}), execute: async () => runtime.run(async (context) => { const body = context.document.body; body.load('text'); await context.sync(); return { text: body.text }; }), }), append_paragraph: tool({ description: 'Add one paragraph to the end of the document.', inputSchema: z.object({ text: z.string().min(1), }), execute: async ({ text }) => runtime.run(async (context) => { context.document.body.insertParagraph(text, 'End'); await context.sync(); return { inserted: true }; }), }), replace_exact_text: tool({ description: 'Replace one exact phrase when it occurs once.', inputSchema: z.object({ search: z.string().min(1), replacement: z.string(), }), execute: async ({ search, replacement }) => runtime.run(async (context) => { const matches = context.document.body.search(search, { matchCase: true, }); matches.load('items'); await context.sync(); if (matches.items.length !== 1) { return { replaced: false, reason: `Expected one match, found ${matches.items.length}.`, }; } matches.items[0].insertText(replacement, 'Replace'); await context.sync(); return { replaced: true }; }), }), }; } ``` Return structured results instead of free-form status text. Refuse ambiguous writes instead of choosing a match for the agent. Use separate tools for comments, tracked proposals, tables, and content controls. For the full walkthrough, see [Build a DOCX agent](/docs/2.x/build-a-docx-agent). For a complete tool catalog, see the [writer agent example](https://github.com/eigenpal/docx-editor/tree/main/examples/write-agent). ### Choose one revision text projection The runtime uses the `allMarkup` text projection by default. Select one projection when you create the runtime: | Content | `allMarkup` | `original` | | ------------------- | ---------------------- | ------------------------------- | | Ordinary text | Visible and searchable | Visible and searchable | | Pending deletion | Visible and searchable | Visible and searchable | | Pending insertion | Visible and searchable | Hidden and not searchable | | Pending replacement | Shows both sides | Shows the deleted original only | `original` matches Word's **Original** review view. ```ts const runtime = DocxEditor.createBrowser(editor, { revisionTextView: 'original', }); await runtime.run(async (context) => { const body = context.document.body; body.load('text'); const matches = body.search('original phrase'); matches.load('items'); await context.sync(); console.log(body.text); }); ``` `revisionTextView` is a DocxEditor runtime option. It does not belong to the Office.js object model. Office.js provides display controls through `document.activeWindow.view.revisionsFilter`. It does not provide a text-projection API for the values returned by text reads. A range returned by a search uses the runtime's text projection. Its `text` property and nested searches use the same projection. The range endpoints remain model offsets, so `insertText()`, `insertComment()`, and `select()` target the text that the search returned. Selection does not change the insertion rule. `range.select()` selects the full range, so the reader's next text insertion replaces that phrase. Use `range.select('End')` to collapse the caret and insert after the phrase. Define app-owned tools, choose a runtime, and keep tracked changes reviewable. The supported subset, how compatibility is verified, and what it omits. ## License This package is licensed under the [EigenPal Pro License](https://github.com/eigenpal/docx-editor/blob/main/packages/editor-api/LICENSE.md), and you can compare and buy license and support levels on the [pricing page](https://www.docx-editor.dev/pricing). ## Next steps - [Build a DOCX agent](/docs/2.x/build-a-docx-agent) - [Office.js compatibility](/docs/2.x/editor-api/office-js-api) - [API reference](/docs/2.x/api/editor-api) --- # Office.js compatibility Source: https://www.docx-editor.dev/docs/2.x/editor-api/office-js-api `@docx-editor.dev/editor-api` implements a subset of the Word JavaScript API object model. You can reuse code based on objects such as `Document`, `Body`, `Paragraph`, and `Range`. You must replace the Office host setup. You must also account for the differences on this page. ## Compatibility matrix **Supported subset** means that the listed objects and operations work. It does not mean that every Office.js member in that area works. | Area | Status | Implemented | Limits | | ---------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Batches and object lifecycle | Supported subset | `load()`, `sync()`, `context.trackedObjects`, `isNullObject`, and null-object accessors | Item accessors need one extra `sync()`. Navigation expansion does not work. | | Document and body | Supported subset | `Document.body`, paragraphs, comments, revisions, sections, content controls, body text, styles, search, clear, and insertion | The package does not provide `Office.onReady` or `Word.run`. | | Paragraphs and ranges | Supported subset | Read text, insert or replace text, insert paragraphs, clear, delete, split, search, style, hyperlinks, and selection | The API does not expose document-wide `start` or `end` offsets. | | Search | Partial | Plain-text search, `matchCase`, and `matchWholeWord` | `ignorePunct`, `ignoreSpace`, and `matchWildcards` compile but refuse `true` with `NotSupported`. | | Font formatting | Partial | `bold`, `italic`, `color`, `name`, and `size` | `underline` and Office.js `highlightColor` do not exist. Mixed, unspecified, or inherited style values return `null`. | | Paragraph formatting | Supported subset | Style, alignment, first-line indent, left indent, right indent, line spacing, space before, and space after | The API exposes these values on `Paragraph`. It does not expose the full `ParagraphFormat` object. | | Lists | Partial | List discovery, list paragraphs, levels, and paragraph insertion | The API does not expose list marker text, sibling indexes, or picture levels. | | Bookmarks | Partial | Discover bookmarks, read names and ranges, and select bookmarks | The API does not support bookmark deletion or document-wide bookmark offsets. | | Sections and page setup | Partial | Section bodies, headers, footers, page size, orientation, and margins | A missing header or footer returns `ItemNotFound`. A read never creates a part. | | Footnotes and endnotes | Supported subset | Enumerate notes, read note bodies and text, move to the next note, and delete notes | Use `document.footnotes` and `document.endnotes`. These accessors differ from Office.js. | | Comments and replies | Partial | Read, create, reply, resolve, delete, and get the comment range | Writes need an explicit author. Browser writes also need the Pro review module and an editable document. Comment body replacement does not work. | | Tracked changes | Partial | Read actionable revisions, get their ranges, and accept or reject one revision or all revisions | The collection omits unsupported structural revision types. A collection-wide decision refuses the full batch if unsupported markup remains. | | Content controls | Partial | Common properties, nested controls, lookup by id, tag, or title, text insertion, deletion, ranges, and typed value writes | The API does not expose typed Office.js subtype objects. Writes to custom XML-bound controls refuse. | | Hyperlinks | Partial | Read or write `Range.hyperlink` | Standalone `Hyperlink` and `HyperlinkCollection` objects do not exist. | | Tables | Unavailable | The editor can display tables | The editing API does not expose `Table`, `TableCollection`, or `TableCell`. | | Images and shapes | Unavailable | The editor can display supported document graphics | The editing API does not expose `InlinePicture`, picture list levels, `Shape`, or canvases. | ## Differences that affect existing code | Office.js behavior | DocxEditor behavior | Required change | | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `Office.onReady` and `Word.run` open a batch. | Your application creates a server or browser runtime. | Use `DocxEditor.createServer(bytes)` or `DocxEditor.createBrowser(editor)`. | | An item accessor returns a usable proxy immediately. | `getFirst()`, `getLast()`, and null-object forms resolve after `sync()`. | Add one `sync()` before you load or change the returned object. | | A collection can load item properties with paths such as `items/text`. | A collection loads its items only. | Load the collection, sync, load each item, then sync again. | | `LoadQueryOptions.expand` loads navigation properties. | A non-empty `expand` value throws `InvalidArgument`. | Load each navigation object or collection directly. | | `sync()` can return a pass-through value. | `sync()` returns `Promise`. | Keep pass-through values in local state. | | The Office host supplies the signed-in comment author. | DocxEditor has no ambient account identity. | Pass `{ author }` when you create the runtime. | | Comment and revision dates have the `Date` type. | Invalid or missing file dates return `null`. | Narrow the nullable `Date` before you use it. | | Font reads use concrete values. | Mixed, unspecified, or inherited style values return `null`. | Narrow the value before you reuse it in a write. | | `Range.select()` runs inside Word. | Selection needs an attached browser editor. | Check `runtime.capabilities.selection`. A server runtime returns `NotSupported`. | | Header and footer getters can create missing parts. | Getters only return existing or inherited parts. | Handle `ItemNotFound` when no part exists. | | `ContentControl.id` is a number. | `ContentControl.id` is a string because DOCX ids can be missing or repeated. | Do not use the file id as a unique numeric key. | | `ContentControl.subtype` uses Word interface terms. | `ContentControl.subtype` uses DOCX control terms. | Handle values such as `plainText`, `dropDownList`, and `checkbox`. | The extra item-accessor sync looks like this: ```ts const results = context.document.body.search('Total'); await context.sync(); const first = results.getFirst(); await context.sync(); first.insertText('TOTAL', 'Replace'); await context.sync(); ``` ## Common DocxEditor additions These common members extend the Office.js-shaped subset. | Member | Purpose | | -------------------------------------------------------- | ---------------------------------------------------------- | | `Body.bookmarks` | Enumerates bookmarks in one body story. | | `Body.revisions` | Enumerates revisions in one body story. | | `Document.footnotes` and `Document.endnotes` | Enumerate document notes. | | `NoteItem.text` | Reads the same plain text as `note.body.text`. | | `ContentControlCollection.getByTag()` and `getByTitle()` | Finds controls without a file id. | | `ContentControl.setValue()` | Writes values for text, checkbox, date, and list controls. | | `ContentControl.isBound` | Reports whether custom XML binding exists. | | `ContentControl.subtype` | Reports the control type with DOCX terms. | | `Comment.text` and `CommentReply.text` | Read comment text without replacing the comment body. | | `Paragraph.uniqueLocalId` | Reads the paragraph identity for the active runtime. | `Body.bookmarks` and `Body.revisions` only cover that body's story. They do not combine the main body, headers, footers, and notes. `ContentControl.isBound` is a preflight check. `sync()` checks the binding again before it applies a write. ## APIs that do not exist Code that uses these APIs fails during TypeScript compilation. | API | Available alternative | | ------------------------------------------------------------ | ---------------------------------------------------------------------------- | | `Table`, `TableCollection`, `TableCell` | No editing API alternative. | | `InlinePicture`, picture list levels | No editing API alternative. | | `Shape` and canvases | No editing API alternative. | | Repeating-section and picture content-control objects | Use common `ContentControl` members for other control kinds. | | `ContentControl.xmlMapping` | Use `ContentControl.isBound` to detect a binding. Bound writes still refuse. | | `Hyperlink`, `HyperlinkCollection` | Use `Range.hyperlink` for one range. | | `Body.getHtml()`, `Body.getOoxml()`, `Paragraph.getText()` | Load `Body.text` or `Paragraph.text`. No OOXML or HTML result exists. | | `BookmarkCollection.exists()` | Load the collection and inspect its items. | | `Font.underline`, Office.js `Font.highlightColor` | No editing API alternative. | | `Range.start`, `Range.end`, `Bookmark.start`, `Bookmark.end` | Use ranges and paragraph text. | ## Migrate an add-in 1. Replace `Word.run()` with a runtime from `DocxEditor.createServer()` or `DocxEditor.createBrowser()`. 2. Keep the existing `load()` and `sync()` pattern. 3. Add the extra syncs listed in [Differences that affect existing code](#differences-that-affect-existing-code). 4. Check the [compatibility matrix](#compatibility-matrix) for every object your add-in uses. 5. Handle typed refusals such as `NotSupported`, `NotImplemented`, and `ItemNotFound`. ## How compatibility is checked The repository keeps a reviewed manifest of supported Word API symbols. CI compares the authored TypeScript declarations with a pinned `@types/office-js` reference. CI also compiles representative Word code against the DocxEditor declarations. The package does not include Microsoft's declarations. Installation, tests, and builds do not fetch them. ## Next steps - [Editing API overview](/docs/2.x/editor-api) - [Editing API reference](/docs/2.x/api/editor-api) --- # Examples Source: https://www.docx-editor.dev/docs/2.x/examples Use a [runnable starter](#runnable-starters) for a complete app. Each starter covers one framework or feature. You can also use the [live demo](#live-demo) on this page. ## Runnable starters You can find each starter in the repository's [`examples/` directory](https://github.com/eigenpal/docx-editor/tree/main/examples). The web starters use `workspace:*` dependencies for `@docx-editor.dev/*` packages. They do not use the published npm packages. Follow these steps to run a web starter: 1. Clone the repository. 2. Run this command from the repository root to install dependencies: ```bash bun install ``` 3. Build the packages if the starter README requires a build: ```bash bun run build:packages ``` The Vite with React and Vite with Vue starters alias package source. These two starters do not need this build. 4. Run the `bun run dev:*` command from the starter README. To use published packages, replace the `workspace:*` versions in the starter's `package.json`. Use the packaged `` component in one file. The host adds New, Open, Save, and a minimal review-rail host. Build a Vite and React single-page app. This starter includes composed controls, the review sidebar, and a custom node. Build a Vue 3 and Vite app with composed controls, navigation, rulers, and package build mode. Use Nuxt 3 or 4 with a client-only editor and server-side rendering. Integrate the editor with the App Router. One dynamic() import uses ssr: false. The rest of the page uses server rendering. Use Remix with Vite. A mount check and lazy() import prevent server rendering for the editor. Render the editor as a React island. The client:only="react" directive disables SSR for the component. Build a themed editor. Host markup and hooks replace the packaged controls. This card opens the deployed demo. Its source is in `examples/igloo`. Define, insert, edit, and round-trip a citation node. The document stores it as a Word content control. Fill a template without a browser or framework. The browser entry can also edit a mounted editor document. Use a large language model (LLM) to read an open document and add anchored comments. Tool calls run in the browser against the open editor. This starter requires an OpenAI key. Read each starter's README for its exact commands. ## Live demo The following demo runs `` on this page. You can open one of your `.docx` files in it. ## Next steps - Follow the [quickstart](/docs/2.x/quickstart) to build a load, edit, and save workflow. - Read [installation options](/docs/2.x/installation) for framework-specific setup. - Read [React composition](/docs/2.x/react/composition) for the primitives used by the Igloo starter. - Read the [editing API guide](/docs/2.x/editor-api) for the headless editing object model. - Follow [Build a DOCX agent](/docs/2.x/build-a-docx-agent) to expose document tools to a model. --- # Markdown Source: https://www.docx-editor.dev/docs/2.x/export/markdown Convert DOCX to Markdown in one call. Get the full document and individual pages, including separate headers, footers, comments, and tracked changes. [Try the demo](https://docx-to-markdown.docx-editor.dev/) · [Integrations](https://github.com/eigenpal/docx-editor/blob/main/packages/docx-to-markdown/docs/integrations.md) · [API reference](https://github.com/eigenpal/docx-editor/blob/main/packages/docx-to-markdown/docs/api.md) ```sh npm install @docx-editor.dev/docx-to-markdown ``` ```ts import { readFile } from 'node:fs/promises'; import { exportMarkdown } from '@docx-editor.dev/docx-to-markdown'; const docxBytes = await readFile('document.docx'); const result = await exportMarkdown(docxBytes); console.log(result.markdown); ``` ## Keep the page numbers The document layout engine calculates page breaks. ```ts for (const page of result.pages) { console.log(page.number, page.markdown); console.log(page.headerMarkdown, page.footerMarkdown); } ``` `result.markdown` joins the body into one document. Headers and footers stay in `result.pages`. For search and AI ingestion, use `{ displayMode: 'proposed' }` to show pending insertions and hide pending deletions. The default, `'all-markup'`, shows both. ## Runtime and output Runs in Node.js with bundled fonts and WebAssembly. Next.js uses the Node.js runtime and [server package configuration](https://github.com/eigenpal/docx-editor/blob/main/packages/docx-to-markdown/docs/integrations.md#nextjs). Edge runtimes are not supported. Page breaks depend on fonts, document features, and revision mode; they can differ from Microsoft Word. Store the document version with page citations. `result.warnings` reports omitted images, shapes, text boxes, and font problems. See [output limits](https://github.com/eigenpal/docx-editor/blob/main/packages/docx-to-markdown/docs/api.md#markdown-limitations) before using the output as a complete transcription. Apache-2.0, including comment and tracked-change extraction. Bundled fonts retain their own open-source licenses. --- # Astro Source: https://www.docx-editor.dev/docs/2.x/frameworks/astro This guide shows an Astro page that mounts the editor as a React island, opens a `.docx` from disk, and downloads the edited result. ## Install ```bash npm install @docx-editor.dev/react @docx-editor.dev/core npx astro add react ``` `astro add react` adds `@astrojs/react` to `astro.config.mjs`. ## Mount the editor The page stays static HTML; the editor is a single island with `client:only="react"`. This is the structure of [`examples/astro`](https://github.com/eigenpal/docx-editor/tree/main/examples/astro): ```astro --- // src/pages/index.astro import { Editor } from '../components/Editor'; --- DOCX editor ``` ```tsx // src/components/Editor.tsx import { useRef, useState } from 'react'; import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/react'; import '@docx-editor.dev/core/styles/editor.css'; export function Editor() { const editorRef = useRef(null); const [buffer, setBuffer] = useState(null); async function onFileSelect(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (file) setBuffer(await file.arrayBuffer()); } async function onSave() { const out = await editorRef.current?.save(); if (!out) return; const blob = new Blob([out], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'document.docx'; a.click(); URL.revokeObjectURL(url); } return (
); } ``` ## Why this pattern `client:only="react"` is required. `client:load` would still server-render the component once to produce static HTML, and SSR throws when `window` is undefined during that pass. `client:only` skips SSR for the island entirely and renders it in the browser only. The rest of the page stays zero-JS static HTML. Styles need no extra setup: the stylesheet import sits inside the island component, and Astro's Vite pipeline bundles CSS imported from `client:only` components like any other module. ## Run the example The example resolves `@docx-editor.dev/*` from built output, so build the workspace packages once first: ```bash git clone https://github.com/eigenpal/docx-editor.git cd docx-editor bun install bun run build:packages bun run dev:astro # http://localhost:4321 ``` ## Next steps - [Quickstart](/docs/2.x/quickstart): the load, edit, save flow in detail - [Loading and saving](/docs/2.x/guides/loading-and-saving): URLs, autosave, upload to your API - [Astro DOCX editor guide](/blog/astro-docx-editor): the detailed walkthrough - [Astro example on GitHub](https://github.com/eigenpal/docx-editor/tree/main/examples/astro) --- # Next.js Source: https://www.docx-editor.dev/docs/2.x/frameworks/nextjs This guide shows an App Router route that opens a `.docx` from disk, edits it in the browser, and downloads the result as a `.docx`. ## Install ```bash npm install @docx-editor.dev/react @docx-editor.dev/core ``` ## Mount the editor Two files. The route stays a client route shell; the editor itself loads from a separate component with `dynamic()` and `ssr: false`. This is the structure of [`examples/nextjs`](https://github.com/eigenpal/docx-editor/tree/main/examples/nextjs). ```tsx // app/page.tsx 'use client'; import dynamic from 'next/dynamic'; const Editor = dynamic(() => import('./components/Editor').then((m) => m.Editor), { ssr: false, loading: () =>
Loading editor...
, }); export default function Page() { return ; } ``` ```tsx // app/components/Editor.tsx 'use client'; import { useRef, useState } from 'react'; import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/react'; import '@docx-editor.dev/core/styles/editor.css'; export function Editor() { const editorRef = useRef(null); const [buffer, setBuffer] = useState(null); const [fileName, setFileName] = useState('document.docx'); async function onFileSelect(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setBuffer(await file.arrayBuffer()); setFileName(file.name); } async function onSave() { const out = await editorRef.current?.save(); if (!out) return; const blob = new Blob([out], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); } return (
); } ``` `document={buffer ?? 'blank'}` starts the user on an empty page, and replaces it with their file when they pick one. Leaving `document` undefined is not the same thing. It means no document at all. The editor shows its loading screen, and every control reports `no document is loaded` until bytes arrive. Use `undefined` only while your fetch is still running. The stylesheet import is required once, and the editor fills its parent so it needs a box with a real height. ## Why this pattern The editor reads the DOM and measures text at mount, so it cannot run during server rendering. If you import `DocxEditor` directly in a route, `next build` fails during prerender with: ```text ReferenceError: window is not defined ``` `dynamic(..., { ssr: false })` is the fix: the editor module is never evaluated on the server, and it stays out of the route's initial bundle. Keep the rest of the page (nav, footer) as server components; only the editor needs the client boundary. ## Run the example The example resolves `@docx-editor.dev/*` from the monorepo workspace, so build the packages once first: ```bash git clone https://github.com/eigenpal/docx-editor.git cd docx-editor bun install bun run build:packages bun run dev:nextjs # http://localhost:3000 ``` ## Next steps - [Quickstart](/docs/2.x/quickstart): the load, edit, save flow in detail - [Loading and saving](/docs/2.x/guides/loading-and-saving): URLs, autosave, upload to your API - [Next.js DOCX editor guide](/blog/nextjs-docx-editor): the detailed walkthrough - [Next.js example on GitHub](https://github.com/eigenpal/docx-editor/tree/main/examples/nextjs) --- # Nuxt Source: https://www.docx-editor.dev/docs/2.x/frameworks/nuxt The editor measures text in the browser. Keep its package import and render work in a client-only component. ## Install Install the published Vue adapter and its engine peer. ```bash npm install @docx-editor.dev/vue @docx-editor.dev/core ``` ## Configure Nuxt Load the editor stylesheet once. Prebundle the adapter and engine for the Vite development server. ```ts // nuxt.config.ts export default defineNuxtConfig({ css: ['@docx-editor.dev/vue/styles.css'], vite: { optimizeDeps: { include: ['@docx-editor.dev/core', '@docx-editor.dev/vue'], }, }, }); ``` ## Add a client-only component Use the `.client.vue` suffix. Nuxt excludes this component from server-side rendering (SSR). ```vue ``` Use the component from a page or layout. Nuxt auto-imports components from the `components` directory. ```vue ``` The server output contains a placeholder for `DocumentEditor`. Nuxt mounts the editor in the browser. The editor fills its parent, so the parent needs a measured height. ## Nuxt module status The repository contains `@docx-editor.dev/nuxt` source and a Nuxt example. The package has `"private": true`, so the repository does not publish it to npm. Use the Vue adapter setup on this page in an external application. The workspace module provides these behaviors: - It registers `` as a client-only component. - It injects the core editor stylesheet by default. - It auto-imports the Vue composables. - It supports a component prefix and optional stylesheet injection. Do not depend on these module behaviors until the package becomes publishable. ## Run the repository example The Nuxt example uses the private module through the monorepo workspace. It runs against the Nuxt version in the workspace. ```bash git clone https://github.com/eigenpal/docx-editor.git cd docx-editor bun install bun run build:packages:vue bun run dev:nuxt ``` Open `http://localhost:3002`. ## Next steps - [Vue adapter](/docs/2.x/vue) - [Vue props](/docs/2.x/vue/props) - [Nuxt example on GitHub](https://github.com/eigenpal/docx-editor/tree/main/examples/nuxt) --- # Remix Source: https://www.docx-editor.dev/docs/2.x/frameworks/remix This guide shows a Remix route that loads a `.docx`, keeps SSR and hydration intact, and downloads the edited file. ## Install ```bash npm install @docx-editor.dev/react @docx-editor.dev/core ``` ## Mount the editor Remix renders routes on the server by default and the editor is browser-only, so the route gates it behind a mount check plus a `lazy()` import. This is the pattern from [`examples/remix/app/routes/_index.tsx`](https://github.com/eigenpal/docx-editor/tree/main/examples/remix): ```tsx // app/routes/_index.tsx import { lazy, Suspense, useEffect, useState } from 'react'; const Editor = lazy(() => import('../components/Editor').then((m) => ({ default: m.Editor }))); export default function Index() { const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []); if (!mounted) return
Loading editor...
; return ( Loading editor...}> ); } ``` The editor component is plain React, no framework-specific code: ```tsx // app/components/Editor.tsx import { useRef, useState } from 'react'; import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/react'; import '@docx-editor.dev/core/styles/editor.css'; export function Editor() { const editorRef = useRef(null); const [buffer, setBuffer] = useState(null); async function onFileSelect(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (file) setBuffer(await file.arrayBuffer()); } async function onSave() { const out = await editorRef.current?.save(); if (!out) return; const blob = new Blob([out], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'document.docx'; a.click(); URL.revokeObjectURL(url); } return (
); } ``` ## Why this pattern The pattern solves two problems, one for SSR hydration and one for bundling: - The `mounted` guard renders the same loading markup on the server and on the first client paint, so hydration never mismatches. Without it, SSR would evaluate the editor and throw when `window` is undefined. - `lazy()` keeps the editor bundle out of the server build and out of the route's initial chunk. ## Run the example The example resolves `@docx-editor.dev/*` from built output, so build the workspace packages once first: ```bash git clone https://github.com/eigenpal/docx-editor.git cd docx-editor bun install bun run build:packages bun run dev:remix # http://localhost:3001 ``` ## Next steps - [Quickstart](/docs/2.x/quickstart): the load, edit, save flow in detail - [Loading and saving](/docs/2.x/guides/loading-and-saving): URLs, autosave, upload to your API - [Remix DOCX editor guide](/blog/remix-docx-editor): the detailed walkthrough - [Remix example on GitHub](https://github.com/eigenpal/docx-editor/tree/main/examples/remix) --- # Vite Source: https://www.docx-editor.dev/docs/2.x/frameworks/vite This guide shows an open-edit-download flow in a Vite + React app. Vite is client-rendered, so there is no SSR boundary to set up. ## Install ```bash npm install @docx-editor.dev/react @docx-editor.dev/core ``` ## Mount the editor Two files, mirroring [`examples/vite`](https://github.com/eigenpal/docx-editor/tree/main/examples/vite). ```tsx // src/main.tsx import { createRoot } from 'react-dom/client'; import { App } from './App'; const container = document.getElementById('app'); if (container) { createRoot(container).render(); } ``` ```tsx // src/App.tsx import { useRef, useState } from 'react'; import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/react'; import '@docx-editor.dev/core/styles/editor.css'; export function App() { const editorRef = useRef(null); const [buffer, setBuffer] = useState(null); const [fileName, setFileName] = useState('document.docx'); async function onFileSelect(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setBuffer(await file.arrayBuffer()); setFileName(file.name); } async function onSave() { const out = await editorRef.current?.save(); if (!out) return; const blob = new Blob([out], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); } return (
); } ``` `document` and `save()` work as described in the [Quickstart](/docs/2.x/quickstart); the only Vite-specific fact is that there is no SSR boundary to manage. The full example also includes composed chrome, the review sidebar, and a custom node. See [`examples/vite`](https://github.com/eigenpal/docx-editor/tree/main/examples/vite) in the repo. For agent-driven editing, see [`examples/agent`](https://github.com/eigenpal/docx-editor/tree/main/examples/agent). ## Run the example The Vite example aliases `@docx-editor.dev/*` to workspace source, so no build step is needed: ```bash git clone https://github.com/eigenpal/docx-editor.git cd docx-editor bun install bun run dev:react # http://localhost:5173 ``` ## Next steps - [Quickstart](/docs/2.x/quickstart): the load, edit, save flow in detail - [Loading and saving](/docs/2.x/guides/loading-and-saving): URLs, autosave, upload to your API - [Vite DOCX editor guide](/blog/vite-docx-editor): the detailed walkthrough - [Vite example on GitHub](https://github.com/eigenpal/docx-editor/tree/main/examples/vite) --- # Vite with Vue Source: https://www.docx-editor.dev/docs/2.x/frameworks/vite-vue Vite runs this Vue app in the browser. You do not need a server-side rendering (SSR) boundary. ## Install ```bash npm install @docx-editor.dev/vue @docx-editor.dev/core ``` ## Add the editor Import the editor stylesheet in `src/main.ts`. ```ts // src/main.ts import { createApp } from 'vue'; import '@docx-editor.dev/vue/styles.css'; import App from './App.vue'; createApp(App).mount('#app'); ``` Add the open, edit, and save flow to `src/App.vue`. ```vue ``` The stylesheet includes the editor chrome and document surface. The editor fills its parent, so `.editor-host` needs a measured height. ## Run the repository example The repository example aliases workspace package source. You do not need to build the packages first. ```bash git clone https://github.com/eigenpal/docx-editor.git cd docx-editor bun install bun run dev:vue ``` Open `http://localhost:5174`. ## Next steps - [Loading and saving](/docs/2.x/guides/loading-and-saving) - [Vue composition](/docs/2.x/vue/composition) - [Vue example on GitHub](https://github.com/eigenpal/docx-editor/tree/main/examples/vue) --- # Chrome slot reference Source: https://www.docx-editor.dev/docs/2.x/guides/chrome-slots Chrome slots are stable control identifiers. Use fixed-command slots with `useEditorCommand()`, `ContextMenu.Slot`, or the generic toolbar button. Use named parts for controls that collect a value or open host UI. ## Read the tables The **Toolbar part** column gives the shared suffix. For example, `Bold` means `DocxEditor.Toolbar.Bold` in React and `DocxEditorToolbar.Bold` in Vue. An em dash means that neither adapter provides a named part. The **Packaged surface** column shows where the standard editor places the control. **Explicit composition** means that you must add its named part to a custom toolbar. Use the named part for controls that collect a value or open UI, such as `FontColor`, `TableBorderWidth`, or `ImageProperties`. Use `Toolbar.Button` in React or `DocxEditorToolbar.Button` in Vue for a fixed-command slot without a named part: #### React ```tsx ``` #### Vue ```vue ``` Use `Action` for a host-owned action without a registry slot. Use `Separator` only for layout. The `Button`, `Action`, and `Separator` helpers don't add slot IDs. ## History, zoom, styles, and font | Slot | Packaged surface | Toolbar part | Purpose | | -------------- | ---------------- | ------------- | ----------------------------- | | `history.undo` | Default toolbar | `Undo` | Undo the last document change | | `history.redo` | Default toolbar | `Redo` | Redo the last undone change | | `zoom.level` | Default toolbar | `Zoom` | Change the viewport zoom | | `styles.style` | Default toolbar | `StylePicker` | Apply a paragraph style | | `font.family` | Default toolbar | `FontFamily` | Choose a font family | | `font.size` | Default toolbar | `FontSize` | Choose a font size | ## Text and script | Slot | Packaged surface | Toolbar part | Purpose | | ---------------- | ---------------- | ------------- | --------------------------- | | `text.bold` | Default toolbar | `Bold` | Toggle bold | | `text.italic` | Default toolbar | `Italic` | Toggle italic | | `text.underline` | Default toolbar | `Underline` | Toggle underline | | `text.strike` | Default toolbar | `Strike` | Toggle strikethrough | | `text.color` | Default toolbar | `FontColor` | Choose and apply text color | | `text.highlight` | Default toolbar | `Highlight` | Choose and apply highlight | | `text.link` | Default toolbar | `Link` | Open the link editor | | `script.super` | Default toolbar | `Superscript` | Toggle superscript | | `script.sub` | Default toolbar | `Subscript` | Toggle subscript | ## Alignment, lists, and formatting The default toolbar combines the four alignment slots into `Alignment`. Use the four `Align*` parts when you want separate controls. | Slot | Packaged surface | Toolbar part | Purpose | | ------------------- | ---------------- | ------------------------ | ------------------------- | | `alignment.left` | Default toolbar | `Alignment`, `AlignLeft` | Align left | | `alignment.center` | Default toolbar | `AlignCenter` | Align center | | `alignment.right` | Default toolbar | `AlignRight` | Align right | | `alignment.justify` | Default toolbar | `AlignJustify` | Justify | | `list.bullet` | Default toolbar | `BulletList` | Toggle a bulleted list | | `list.numbered` | Default toolbar | `NumberedList` | Toggle a numbered list | | `list.outdent` | Default toolbar | `Outdent` | Decrease list indent | | `list.indent` | Default toolbar | `Indent` | Increase list indent | | `list.lineSpacing` | Default toolbar | `LineSpacing` | Set line spacing | | `format.painter` | Default toolbar | — | Copy and apply formatting | | `format.clear` | Default toolbar | `ClearFormatting` | Clear direct formatting | | `paragraph.dialog` | Format menu | — | Open paragraph settings | ## Review and content controls Comments and reviewer filtering require the Pro review module. Content-control slots require the typed content-control capability. The two surface toggles can be composed anywhere; inspector and remove become enabled when the selection supports them. | Slot | Packaged surface | Toolbar part | Purpose | | -------------------------- | -------------------- | ------------------------- | ------------------------------- | | `review.comments` | Default toolbar | `Comments` | Open comments and changes | | `review.authors` | Review menu | `Reviewers` | Filter markup by reviewer | | `review.editingMode` | Default toolbar | `EditingMode` | Select editing or review mode | | `contentControl.showAll` | Explicit composition | `ContentControlShowAll` | Show content-control boundaries | | `contentControl.formFill` | Explicit composition | `ContentControlFormFill` | Toggle form-fill mode | | `contentControl.inspector` | Explicit composition | `ContentControlInspector` | Open content-control properties | | `contentControl.remove` | Explicit composition | `ContentControlRemove` | Remove the selected control | ## Images and tables The toolbar shows image controls when you select an image. It shows table border and fill controls when you place the caret or a cell selection in a table. The Insert menu also provides image and table insertion. | Slot | Packaged surface | Toolbar part | Purpose | | -------------------- | -------------------------- | ------------------- | ------------------------------ | | `image.insert` | Insert menu, image context | `ImageInsert` | Insert an image | | `image.properties` | Image context | `ImageProperties` | Open image properties | | `image.wrap` | Image context | `ImageWrap` | Choose text wrapping | | `image.altText` | Image context | `ImageAltText` | Edit alternative text | | `table.insert` | Insert menu | `TableInsert` | Insert a table | | `table.borderTarget` | Table context | `TableBorderTarget` | Choose which borders to affect | | `table.borderColor` | Table context | `TableBorderColor` | Choose and apply border color | | `table.borderStyle` | Table context | `TableBorderStyle` | Choose border style | | `table.borderWidth` | Table context | `TableBorderWidth` | Choose border width | | `table.cellFill` | Table context | `TableCellFill` | Choose and apply cell fill | ## File and insert The packaged menus provide file, break, note, and table-of-contents slots. Header/footer options provide page-field slots. | Slot | Packaged surface | Toolbar part | Purpose | | ------------------------------- | --------------------- | ------------ | ------------------------------------- | | `file.open` | File menu | — | Load a document | | `file.save` | File menu | `Save` | Save a document | | `file.pageSetup` | File menu | — | Open page setup | | `insert.footnote` | Insert menu | — | Insert a footnote | | `insert.endnote` | Insert menu | — | Insert an endnote | | `insert.pageNumber` | Header/footer options | — | Insert the current page number | | `insert.totalPages` | Header/footer options | — | Insert the document page count | | `insert.sectionPages` | Header/footer options | — | Insert the current section page count | | `insert.pageXofY` | Header/footer options | — | Insert a Page X of Y field pair | | `insert.pageBreak` | Insert menu | — | Insert a page break | | `insert.sectionBreakNextPage` | Insert menu | — | Start a section on the next page | | `insert.sectionBreakContinuous` | Insert menu | — | Start a section on the current page | | `insert.toc` | Insert menu | — | Insert a table of contents | ## Use a slot directly For a fixed-command slot, `useEditorCommand(slot)` supplies enabled state, active state, the engine's disabled reason, and `execute()`. Value and dialog controls also need input from your UI. Pass their complete `EditorCommand` to `useEditorCommand()`, or use the packaged part or menu. Handle `file.open` and `file.save` with the document loading and saving APIs. For complete signatures, see the [core API reference](/docs/2.x/api/core), [React API reference](/docs/2.x/api/react), or [Vue API reference](/docs/2.x/api/vue). ## Next steps - [Customize the toolbar](/docs/2.x/guides/toolbar) - [Compose a React editor](/docs/2.x/react/composition) - [Compose a Vue editor](/docs/2.x/vue/composition) --- # Content controls Source: https://www.docx-editor.dev/docs/2.x/guides/content-controls Word content controls (`w:sdt`, structured document tags) mark regions you can fill from code. Create a template in Word, tag its fillable regions, and use `tag`, `title`, or `id` to find each control. The editor displays control boundaries and supports the editing operations below. It preserves unsupported properties, including `w:dataBinding` and `w15:repeatingSection`, when you save. Content controls are addressed by `tag`, `title`, or `id`. They are not `{{ mustache }}` template variables; the two systems can coexist in one document. See the [Word fidelity matrix](/docs/2.x/word-fidelity) for support levels across features. ## Filling a template from a server [`@docx-editor.dev/editor-api`](/docs/2.x/editor-api) is the headless path, and its object model is Office.js-compatible. Reads are batched, then one `sync()` sends the writes as a single ordered batch: ```ts import { readFile, writeFile } from 'node:fs/promises'; import { DocxEditor } from '@docx-editor.dev/editor-api'; const runtime = await DocxEditor.createServer(await readFile('contract-template.docx')); try { await runtime.run(async (context) => { const controls = context.document.body.contentControls; controls.load(); await context.sync(); // one round trip: now you know what the template has for (const control of controls.items) { if (control.tag === 'customerName') control.setValue({ kind: 'text', text: 'Acme GmbH' }); if (control.tag === 'effective') control.setValue({ kind: 'date', iso: '2026-07-01' }); if (control.tag === 'agree') control.setValue({ kind: 'checkbox', checked: true }); if (control.tag === 'betaClause') control.delete(false); // condition not met } await context.sync(); // one atomic batch: all of the writes, or none }); await writeFile('contract-acme.docx', Buffer.from(await runtime.save())); } finally { runtime.dispose(); } ``` `getByTag(tag)` narrows the collection directly when you know what you are looking for, and `getById(id)` / `getFirstOrNullObject()` address a single control. ## Values are typed `setValue` takes a discriminated value rather than a bare string, so a typed control cannot be filled with something it cannot hold: | Kind | Shape | For | | ---------- | ------------------------------- | ------------------------------------------------------ | | `text` | `{ kind: 'text', text }` | Rich-text and plain-text controls. | | `listItem` | `{ kind: 'listItem', value }` | Dropdowns and combo boxes. Must match a declared item. | | `checkbox` | `{ kind: 'checkbox', checked }` | Checkbox controls. | | `date` | `{ kind: 'date', iso }` | Date pickers. `YYYY-MM-DD` or a full ISO-8601 instant. | `insertText(text, 'Replace' | 'Start' | 'End')` writes free text where that is what you want, and `delete(keepContent)` either drops a control with its content or unwraps it and keeps the content in place. ## Reading state before writing `placeholderShown` tells you the control still holds Word's boilerplate ("Click here to enter text") rather than real data. Check it before treating `text` as entered content. `cannotEdit` and `cannotDelete` expose the control's locks, and are settable when you are producing a template rather than filling one. ```ts controls.load(); await context.sync(); const unfilled = controls.items.filter((c) => c.placeholderShown); ``` ## In the editor `useContentControl()` is the live equivalent. It reports the control at the caret, whether it can be written, and why not when it cannot: #### React ```tsx import { useContentControl } from '@docx-editor.dev/react'; function ControlInspector() { const { control, setValue, canSetValue, setValueDisabledReason, remove, canRemove } = useContentControl(); if (!control) return null; return (

{control.alias ?? control.tag ?? control.id}

{control.controlType}

); } ``` #### Vue ```vue ``` The inspector state carries `tag`, `alias`, `id`, `controlType`, `locked`, `removalLocked`, `effectiveLock`, `bound` (whether it is driven by a data binding), and `placeholder`. `showAll` / `setShowAll` toggles boundary rendering for every control, and `formFill` / `setFormFill` puts the document into fill-only mode, where the caret can enter controls but not the text around them. The packaged `ContentControl` part is the inspector panel over exactly this API, and `CONTENT_CONTROL_SLOTS` lists the matching chrome slot ids. A data-bound control rejects a direct write: its content comes from the Custom XML store, so writing it here would not persist in Word. `setValueDisabledReason` reports the reason instead of dropping the write. ## Creating a control in the editor `insertContentControl` authors a new control in the open document, as one undoable step. Select text to wrap it in a control, or leave the caret where it is to insert an empty control that shows Word's prompt for its type: #### React ```tsx import { useDocxEditor } from '@docx-editor.dev/react'; function InsertFieldButton() { const editor = useDocxEditor(); return ( ); } ``` #### Vue ```vue ``` `tag` is the identity you look the control back up by, and `title` is the label Word shows. An empty control holds its prompt until the user types: the first character replaces the whole prompt, the way it does in Word. To address text other than the selection, pass a `target` that names a paragraph by its `paraId` and the phrase inside it: ```ts editor.exec({ type: 'insertContentControl', target: { paraId: '1B4C77A2', search: 'Acme GmbH' }, subtype: 'plainText', tag: 'customerName', }); ``` You can author `richText`, `plainText`, `dropdown`, `comboBox`, and `date` controls. OOXML also spells `dropdown` as `dropDownList`. A new dropdown or combo box has no items. Add its items in Word, or fill a control from an existing template. `editor.can()` answers the same refusal `exec` would, so a button can disable itself and show the reason. | Operation | Refusal condition | Reason | | ------------ | ------------------------------------------------------------ | --------------------------------------------- | | Fill or edit | The control or an ancestor locks its content | `locked` | | Remove | The control or an ancestor locks its wrapper | `locked` | | Fill | The control declares `w:dataBinding` | `bound` | | Fill | The value does not match the control type | `typeMismatch` | | Create | The selection crosses paragraphs | Inline controls cannot wrap block content | | Create | The position is inside a hyperlink, field, or inline control | The new control has no valid sibling position | ## Checkbox, dropdown, and date interactions Select the button at the upper-right corner of a control to toggle a checkbox, open a dropdown menu, or open a date picker. Each edit supports undo and requires no application event handler. Checkbox toggles use MS Gothic when the document omits the state font. These interactions apply to content controls. For legacy Word form fields, see [Legacy text form fields](/docs/2.x/guides/fields#legacy-text-form-fields). ## Operational limits - The editor does not support a control around a whole table cell or row. Controls inside a cell work. - The editor does not surface a control inside a hyperlink. - `w:dataBinding` round-trips without bound-value resolution. - Repeating-section markup round-trips without item add, item remove, or section configuration edits. - Control creation, filling, and removal do not create revisions in suggesting mode. ## Next steps - [Editing API](/docs/2.x/editor-api): the full object model and its batching rules - [Custom nodes](/docs/2.x/pro/custom-nodes): your own typed inline nodes over the same OOXML primitive - [React hooks](/docs/2.x/react/hooks) and [Vue composables](/docs/2.x/vue/composables) --- # Custom styles and branding Source: https://www.docx-editor.dev/docs/2.x/guides/custom-styles Load a styled `.docx` template to apply your styles and fonts from a template. The editor uses the styles defined in the document itself (headings, fonts, colors, spacing, and table styles) and preserves them when users save. Templates can define: - Heading styles (`Heading1` to `Heading9`) - Body text (`Normal`) - Fonts and themes - Table styles and the default table style Any content users create or insert inherits those styles from the document style definitions. ## Quick start Create a `.docx` containing your brand styles, remove its content, and load it whenever a user starts a new document. #### React ```tsx import { useEffect, useState } from 'react'; import { DocxEditor } from '@docx-editor.dev/react'; import '@docx-editor.dev/core/styles/editor.css'; export function BrandedEditor() { const [template, setTemplate] = useState(); useEffect(() => { fetch('/templates/brand.docx') .then((r) => r.arrayBuffer()) .then(setTemplate); }, []); return ; } ``` #### Vue ```vue ``` ## Create a template ### Use a word processor 1. Create a document with your desired styles. 2. Define any table styles you use (see [Tables](#tables)). 3. Remove the document content. 4. Save the file as your template. ## Headings and paragraph styles The style dropdown is populated from the paragraph styles defined in the document. When a user applies a style, the editor uses the document's definition for fonts, colors, spacing, and the configured `next` style. For example, pressing Enter after a heading switches back to body text. Set `w:qFormat` to surface a style in the dropdown, and `w:semiHidden` or `w:hidden` to keep one out. ## Tables Table styles defined in the document (borders, shading, banded rows, header-row formatting) render through the style cascade, including `basedOn` inheritance, and survive a round trip. Newly inserted tables do not pick up a document default table style: Insert → Table authors an even grid with explicit single-line borders, so it renders the same in every document. Restyle it afterwards with the table border and fill controls. ## Fonts Provide brand fonts through the editor's `fonts` prop. `loadFonts` fetches the URLs you list and returns a fragment the editor both measures and paints with: #### React ```tsx import { loadFonts } from '@docx-editor.dev/react'; const brand = await loadFonts({ sources: [ { url: '/fonts/CustomSans-Regular.ttf', family: 'Custom Sans', weight: 400, style: 'normal' }, { url: '/fonts/CustomSans-Bold.ttf', family: 'Custom Sans', weight: 700, style: 'normal' }, ], }); ; ``` #### Vue ```vue ``` The font picker offers the families the document declares plus the ones you configure. See [React props: Fonts](/docs/2.x/react/props#fonts) or [Vue props: Fonts](/docs/2.x/vue/props#fonts) for the prop, and [Fonts and measurement](/docs/2.x/guides/fonts) for hashes, failure handling, and on-demand loading. A font that is only installed on the user's system or registered through your own `@font-face` rules affects painting, not measurement: the editor measures with the bytes you hand it and falls back to estimated metrics for families it has no bytes for. Fonts embedded inside a `.docx` are loaded automatically: the editor de-obfuscates the embedded faces, renders with them, and lists them in the font picker. They are also preserved when saving. ## Style preservation The editor preserves existing style files (`styles.xml`, `theme1.xml`, `settings.xml`) rather than regenerating them. Custom styles, themes, fonts, and table defaults survive a full edit-and-save round trip. ## Next steps - [Fonts and measurement](/docs/2.x/guides/fonts): font sources and how they compose - [Word fidelity](/docs/2.x/word-fidelity): what survives a round trip - [React props](/docs/2.x/react/props) and [Vue props](/docs/2.x/vue/props): `fonts` and the rest of the root props --- # Dark mode Source: https://www.docx-editor.dev/docs/2.x/guides/dark-mode Set `colorMode` to theme the chrome and document canvas the way Word's dark view does. ## Enabling dark mode `colorMode` is a controlled prop. There is no internal toggle button; you set the value and the editor uses that value. | Value | Behavior | | ---------- | ---------------------------------------------------------------------------------------------------- | | `'light'` | Light theme (default). | | `'dark'` | Dark theme. | | `'system'` | Follows the operating system via `prefers-color-scheme`, and updates live when the OS theme changes. | #### React ```tsx ``` #### Vue ```vue ``` ## Toggling from your own UI Wire `colorMode` to your own control: a switch, a settings menu, a segmented button. The editor applies the new theme when the value changes. #### React ```tsx const [colorMode, setColorMode] = useState<'light' | 'dark'>('light'); ``` #### Vue ```vue ``` To follow the operating system instead, pass `colorMode="system"`. ## What dark mode changes Dark mode has two layers: - **Editor chrome** (toolbar, title bar, dialogs, dropdowns, sidebar, menus) re-themes through the shared design tokens, so every surface tracks the theme. - **The document canvas** is rendered like Word's dark view: the page turns dark and text turns light. Rather than swapping two fixed colors, the canvas applies a perceptual transform that inverts each authored color's lightness while preserving its hue: black body text becomes near-white, a dark-navy heading becomes light blue, a dark red stays red. Authored colors keep contrast under the lightness transform. It is a display transform only. Dark mode never changes the document itself: - Authored document colors are preserved; the display transform is never written into the DOCX. - Printing always uses a white page with black text, regardless of `colorMode`. - Embedded images, logos, and screenshots are not inverted; they render as authored. The light theme is unchanged when `colorMode` is `'light'`. ## Reporting issues Dark mode covers chrome and the document canvas, and the canvas transform is heuristic. If a control does not use the theme correctly, open an issue at [GitHub issues](https://github.com/eigenpal/docx-editor/issues). Also report text with low contrast or colors that transform incorrectly. Mention `colorMode` and include a screenshot. ## Next steps - [React props](/docs/2.x/react/props#dark-mode) and [Vue props](/docs/2.x/vue/props#appearance): the `colorMode` prop - [Custom styles](/docs/2.x/guides/custom-styles): theming the chrome around the canvas - [React composition](/docs/2.x/react/composition) and [Vue composition](/docs/2.x/vue/composition): build chrome that themes with your app --- # Fields and cross-references Source: https://www.docx-editor.dev/docs/2.x/guides/fields Word fields contain an instruction and an optional saved result. The editor preserves both forms when it opens and saves a DOCX file. The editor evaluates a defined set of display-only fields. It never runs macros, DDE instructions, or external include instructions. ## Supported fields | Field | Behavior | | ----------------------------------------- | ------------------------------------------------------------------- | | `PAGE`, `NUMPAGES`, and `SECTIONPAGES` | Update in headers and footers. Empty-cache body fields also update. | | `REF` | Resolves bookmark text or numbering in the document body and notes. | | `NOTEREF` | Resolves a bookmarked footnote or endnote reference number. | | `AUTONUM`, `AUTONUMLGL`, and `AUTONUMOUT` | Generate separate document-order number sequences. | | `SYMBOL` | Renders the requested character, font, and size. | | Selected metadata fields | Render sanitized values from document properties. | The editor also renders saved results for other complex and simple fields. Unsupported instructions keep their saved result. ## `REF` fields `REF` resolves bookmark text and numbered paragraph references. The editor supports the `\r`, `\w`, `\n`, `\t`, `\h`, and `\* MERGEFORMAT` switches. The `\t` switch removes text that is not a number delimiter from a referenced number. It needs `\r`, `\w`, or `\n`. The `\h` switch parses without adding hyperlink behavior. The editor checks a non-empty saved result once. A matching result enables live updates after later edits. A mismatch keeps Word's saved result for that field. An empty saved result remains eligible for live updates. The editor also keeps the saved result for missing bookmarks, bullet targets, and unsupported switches. Bookmark text resolution stops at the target paragraph boundary. The `\r` switch uses the same full-context number as `\w`. It does not calculate Word's relative number. Live `REF` resolution covers the document body, footnotes, and endnotes. Header, footer, and text-box results keep their saved values. ## `NOTEREF` fields `NOTEREF` resolves the display number of a bookmarked footnote or endnote reference. It honors section number formats and `eachSect` restarts. The editor keeps the saved result for `\p`, `\f`, `eachPage` restarts, and custom note marks. The `\h` switch does not add hyperlink behavior. ## `AUTONUM` fields Each `AUTONUM` field kind has its own sequence. The editor numbers each sequence in document order. Word can restart these counters by heading context. The editor does not apply that restart behavior. The `\*` switch supports Arabic, alphabetic, Roman, ordinal, cardinal text, ordinal text, and hexadecimal formats. The `\e` switch removes the trailing period. An unsupported switch produces no generated value. Save does not add result runs because Word does not store results for these fields. ## Save-time refresh `save()` updates calibrated, writable `REF` and `NOTEREF` results in the body, footnotes, and endnotes. It commits all updated parts in one undo step. The editor skips locked fields, revision markup, nested or unsafe result structures, and protected content controls. A skipped field keeps its saved result. View mode and read-only sessions do not rewrite results. A collaborative session also exports the saved results without rewriting them. ## Fields that stay inert `DATE`, `TIME`, `FILENAME`, `SEQ`, `LISTNUM`, `EQ`, `CITATION`, and `BIBLIOGRAPHY` do not calculate a new value. The editor displays a saved result when one exists. The editor preserves field instructions through save. It does not execute macros, DDE instructions, OLE content, or external include instructions. ## Legacy text form fields Select a `FORMTEXT` field, then double-click it or choose **Edit field…** from the context menu. Set its default value, type (regular text, number, or date), maximum length, format, and **Fill-in enabled** setting. A maximum length of zero means unlimited. React and Vue use the same dialog. In an unprotected document, partial edits keep the field definition; replacing its whole result removes it. In a document protected for forms, editing keeps the definition. Tab and Shift+Tab move between enabled text fields. Protected fields validate and format input when you leave. Pasted text is limited to the remaining capacity. Invalid numbers or dates open an alert; acknowledging it clears the input, which can be restored with Undo. Save also validates pending protected field input and applies its format. Invalid input rejects the save with code `invalidArgs`. The input stays available for correction, and save does not open an alert. ### Date input and UI language `locale` controls date input; `i18n` controls UI strings. For Polish dates with the default English UI, use: ```tsx ``` With `pl-PL`, `01.02.2030` means February 1. The default `en-US` interprets `01/02/2030` as January 2. ISO input (`2030-02-01`) works in every locale. Each field's format controls its displayed result. Changing locale preserves existing dates and applies to subsequent edits. Date input supports Gregorian numeric dates, regional digits, and full English month names. Localized month names and non-Gregorian dates are not supported. Only plain text results and the dialog's listed types and formats support editing and protected filling. The editor preserves other field structures. Legacy checkbox and dropdown form fields are not interactive; use [content controls](/docs/2.x/guides/content-controls) for those interactions. Entry and exit macros never run. ## Next steps - [Headers and footers](/docs/2.x/guides/headers-footers): Add page-number fields. - [Loading and saving](/docs/2.x/guides/loading-and-saving): Save updated field results. - [Word fidelity](/docs/2.x/word-fidelity): Review field support status. --- # Fonts and measurement Source: https://www.docx-editor.dev/docs/2.x/guides/fonts The editor uses HarfBuzz when it has usable font bytes. Without them, a browser uses canvas measurement when available. Other environments use fixed measurement. The document still opens with fallback measurement. Neither fallback guarantees Word-compatible line wraps or page breaks. ## Start here Every font origin has the same shape: call it, and pass the result. To add another origin, add another argument. ```ts import { packagedFonts } from '@docx-editor.dev/fonts'; const { document, fonts } = useDocxSource(url, { fonts: packagedFonts() }); ``` ```ts import { packagedFonts } from '@docx-editor.dev/fonts'; import { googleFonts } from '@docx-editor.dev/fonts/google'; const { document, fonts } = useDocxSource(url, { fonts: [packagedFonts(), googleFonts()], }); ``` The list is a precedence order: the first origin that supplies a face wins, and later origins receive the resolved faces so they can skip duplicate downloads. Put preferred sources first. `googleFonts()` fetches from a content delivery network. `packagedFonts()` and `defaultFonts()` load assets shipped with the package, typically from your own origin in a browser. No optional font source is enabled by default. ## Choose a font source | Source | Third-party requests | Loads | Measurement | | -------------------------- | ---------------------------- | --------------------------------------------------- | ---------------------------------- | | Fonts embedded in the DOCX | No | The faces in the file | Uses the embedded bytes | | `packagedFonts()` | No | The packaged families in use, plus the default face | Uses metric-compatible substitutes | | `defaultFonts()` | No | All 20 faces of the five defaults, every time | Uses metric-compatible substitutes | | `googleFonts()` | Yes, for catalogued families | The catalog families in use, plus the default face | Uses the fetched faces | | `loadFonts` with your URLs | Depends on your URLs | The URLs you list | Uses admitted files | | No usable source | No | Nothing | Canvas or fixed fallback | Embedded fonts load automatically. The editor registers them under internal aliases. A document cannot replace a page-wide font family used by your application. ### Understand the font notice The packaged font notice lists rendered document families without an available compatible face. It excludes metric-compatible substitutions. The notice also excludes font declarations that rendered text does not use. The font picker can still list those declared families. The notice also excludes symbol faces, such as MS Gothic in a Word checkbox (`w:sym`). The editor maps symbols to Unicode where a mapping exists. Rendering still depends on the available font glyphs; a missing notice does not guarantee that every symbol can render. The font picker is a separate list. It offers the families your configuration supplies and the families the document declares in `w:rFonts`, so a symbol face appears there when the file declares it as a run font, which Word's checkbox markup does. ## Load Word-compatible defaults The optional `@docx-editor.dev/fonts` package provides open-licensed substitutes. | Word font | Substitute | License | Loaded by default | | --------------- | ----------------- | ----------------- | ----------------- | | Calibri | Carlito | SIL OFL | Yes | | Cambria | Caladea | SIL OFL | Yes | | Times New Roman | Liberation Serif | SIL OFL | Yes | | Arial | Liberation Sans | SIL OFL | Yes | | Courier New | Liberation Mono | SIL OFL | Yes | | Century Gothic | TeX Gyre Adventor | GUST Font License | No | The first five match advance widths exactly. Rare kerning differences can still change an edge case. TeX Gyre Adventor is close, not exact. It runs slightly narrow against Century Gothic across the recorded samples: 0.22%, 0.34%, 0.67%, and 0.85% at 40 pt bold. `bun run check:font-width-fidelity` holds it within 1%. Over a long line that is a fraction of a character, and a wrap point can still move. Supply licensed font bytes when pixels must match the original face. `defaultFonts()` loads the five families Word applies to a document by default. Century Gothic is not one of them, and its four assets add about 709 KB to every load, so you opt in: ```ts import { ALL_WORD_DEFAULT_FAMILIES, defaultFonts } from '@docx-editor.dev/fonts'; const fonts = await defaultFonts({ families: ALL_WORD_DEFAULT_FAMILIES }); ``` `googleFonts()` covers it on demand instead. It serves Century Gothic from the same packaged bytes, and only when a document names the family, so it makes no third-party request for it. `packagedFonts()` loads a family only when a document names it, or when it is that document's default face. Narrow it further with `allow`: ```ts const fonts = packagedFonts({ allow: ['Calibri'] }); ``` Font binaries load as separate assets. Importing the package fetches nothing. ### Choose between lazy and eager loading `packagedFonts()` and `defaultFonts()` measure identically for the families they share. They differ in when they run, what they cover, and what that costs. | Behavior | `packagedFonts()` | `defaultFonts()` | | ---------------------------- | --------------------------------------------------- | -------------------------------- | | Runs | After the document is parsed | Before the document opens | | Covers | All six substituted families | The five Word applies by default | | Loads | The families in the document, plus the default face | All 20 faces, 7.4 MB | | First layout | Fixed measurement, then re-paginates | Correct on the first pass | | Undo history across the swap | Cleared when the faces arrive | Kept | Century Gothic is the family that difference is for. `defaultFonts()` leaves it out because it would cost every document about 709 KB for a family most never name. `packagedFonts()` serves it from the same bundled bytes when a document asks. Use `defaultFonts()` when a visible re-pagination is worse than the extra bytes. Use `packagedFonts()` otherwise. ```ts import { defaultFonts } from '@docx-editor.dev/fonts'; const { document, fonts } = useDocxSource(url, { fonts: defaultFonts }); ``` With `defaultFonts`, `useDocxSource` holds `document` back until the fonts settle, so the reader never sees the text reflow. An on-demand resolver has nothing to hold for: the families it answers about come from the parse, so the bytes have to go through first. If you load default bytes separately, pass them to the paint-side installer. This avoids a second request for the same font files: ```ts import { installDefaultFontFaces, loadDefaultFonts } from '@docx-editor.dev/fonts'; const loaded = await loadDefaultFonts(); await installDefaultFontFaces({ loaded: loaded.sources }); ``` ## Load your own fonts Use `loadFonts` for brand fonts or licensed Word fonts. ```ts import { loadFonts } from '@docx-editor.dev/core/editor'; const brand = await loadFonts({ sources: [ { url: '/fonts/AcmeSans-Regular.ttf', family: 'Acme Sans', weight: 400, style: 'normal', }, { url: '/fonts/AcmeSans-Bold.ttf', family: 'Acme Sans', weight: 700, style: 'normal', hash: 'sha256:…', }, ], }); ``` `brand` is a fragment. Compose it with the packaged substitutes the same way you compose any two origins, and put it first so your own faces win: ```ts import { packagedFonts } from '@docx-editor.dev/fonts'; const { document, fonts } = useDocxSource(url, { fonts: [brand, packagedFonts()] }); ``` | `loadFonts` behavior | Result | | -------------------- | ---------------------------------------------------------------------- | | One source fails | Returns admitted sources and a typed `failures` list. | | Hash matches | Admits the source. | | Hash differs | Rejects that source with `hashMismatch`. | | Hash omitted | Computes one for the returned source. | | Same request repeats | Uses the Cache API when available. Otherwise, each call fetches again. | Pin every URL that you do not control. To create pins, load once without a hash and store the returned source hashes. ```ts const result = await loadFonts({ sources }); console.log(result.sources.map((source) => `${source.id}: ${source.hash}`)); ``` Use `createFontSource` when you already have bytes from a file input, IndexedDB, or a bundler. ```ts import { composeFontConfiguration, createFontSource } from '@docx-editor.dev/core/editor'; const made = createFontSource(bytes, { family: 'Acme Sans', weight: 400, style: 'normal', }); if ('source' in made) { const fonts = composeFontConfiguration({ sources: [made.source] }); } else { report(made.failure.reason); } ``` This function returns a typed failure instead of throwing for invalid descriptors or bytes. ## Load fonts on demand `packagedFonts()` and `googleFonts()` are resolvers. The editor calls a resolver once per document load, after parsing, with the family names that document declares. `useDocxSource` handles the identity of a resolver for you. When you pass one directly to the `fonts` prop, use `useFonts` instead: an inline resolver is a new function on each render, and that identity change rebuilds the editor. #### React ```tsx import { packagedFonts } from '@docx-editor.dev/fonts'; import { googleFonts } from '@docx-editor.dev/fonts/google'; import { DocxEditor, useFonts } from '@docx-editor.dev/react'; function Editor({ bytes }: { bytes: Uint8Array }) { const fonts = useFonts(packagedFonts(), googleFonts()); return ; } ``` #### Vue ```vue ``` Every argument to `useFonts` takes the same union: a resolver, a fragment, a configuration, or a promise for one. Arguments compose first-wins, in order. `googleFonts()` uses a generated catalog. Each face is pinned to an immutable `google/fonts` commit and includes a checked `sha256:` value. Most faces share one commit. A family whose current upstream version ships variable-only files is pinned to the last commit that carried static instances, so the catalog records more than one revision. ```ts googleFonts({ allow: ['Tinos', 'Carlito'], substitute: { Georgia: 'Tinos' }, }); ``` An arbitrary substitute can change line breaks and pagination. Use a metric-compatible substitute when layout must remain stable. | Resolver constraint | Required behavior | | -------------------------- | --------------------------------------------------------------------- | | File-supplied family names | Match them against a closed `Map` or catalog. | | URL construction | Never build a URL from a document family name. | | Privacy | A remote font host can learn which families the document uses. | | Updates | Load another document or remount to apply changed resolver arguments. | | Composition | `useFonts(packagedFonts(), googleFonts())` combines sources. | The editor caps the family list passed to a resolver. `googleFonts()` resolves a family against closed sets, in this order: 1. A redirect from the substitution map: your `substitute` entries merged over the built-in metric map, such as Calibri to Carlito. Your entries win, and they win over the other two steps as well, so you can redirect any family. 2. A packaged face for a family the catalog cannot match. Century Gothic is the one. It reads the package's own assets, so it makes no third-party request. 3. A direct catalog match on the name the document wrote. Step 1 comes first, so `substitute: { Lato: 'Tinos' }` redirects Lato even though Lato is catalogued under its own name. A family that none of the three answers resolves to nothing, and keeps whatever measurement your host already had. That is deliberate. Only a metric-compatible substitute keeps pagination Word-accurate, and a face picked on how a font describes itself is not one: `word/fontTable.xml` states a PANOSE classification, never an advance width, so nothing in the file bounds how much wider the substitute would run. ### Serve the catalog from your own origin `googleFonts()` fetches from a content delivery network. To send those requests somewhere else, pass `fetcher`. It replaces the `fetch` that resolver uses, so your function decides where each catalogued face comes from: ```ts googleFonts({ fetcher: (input) => fetch(rewriteToYourMirror(String(input))), }); ``` The bytes suit a mirror well. Each face is pinned to an immutable commit and carries a `sha256:` value. The engine re-derives that hash when it admits the bytes, so a substituted or corrupted file fails loudly rather than rendering. `packagedFonts()` and `defaultFonts()` accept `fetcher` too, and their assets ship inside the package, so they read your own origin with or without it. Both also register faces for painting, and that step can reach the browser's own `FontFace` loader, which takes a URL rather than your function. To be certain every byte goes through `fetcher`, pass `install: false` to `packagedFonts()`. Measurement is unaffected: painted glyphs then fall back to whatever the platform substitutes for the family name. `defaultFonts()` has no such option, so call `loadDefaultFonts()` instead when you need that guarantee. Use `fetcher` for caching too. `googleFonts()` keeps its catalog fetches in memory, keyed by the fetcher you passed, which is what a browser tab needs. Nothing else caches. The packaged assets are re-read on every call. A server that renders in short-lived processes gets no reuse either way. Nothing is written to disk, and nothing is shared between workers. Wrapping `fetcher` is where you add that: ```ts googleFonts({ fetcher: async (input) => { const url = String(input); const hit = await yourCache.get(url); if (hit) return new Response(hit); const response = await fetch(url); const bytes = new Uint8Array(await response.clone().arrayBuffer()); await yourCache.set(url, bytes); return response; }, }); ``` ### Run without access to the network A face that cannot be fetched is dropped, and the family it belonged to measures on the engine's fixed fallback instead. Treat that as a layout problem, not a cosmetic one. Substitute bytes are what keep wrapping and pagination close to Word. A blocked request therefore changes where your pages break, not only how the text looks. The document still opens. Each dropped face goes to the resolver's own `onFailure` option, which writes a console warning when you pass no handler: ```ts googleFonts({ onFailure: ({ family, url, diagnostic }) => report(family, url, diagnostic), }); ``` Two options work when your deployment cannot reach a content delivery network. Point `fetcher` at a mirror you control. Or leave `googleFonts()` out and use `packagedFonts()` alone, which reads only the bytes inside the package. ### Write your own resolver Wrap it in `defineFontResolver`. That mark is how `useDocxSource` tells a resolver, which it calls with a request, from a loader such as `defaultFonts`, which it calls with no arguments. TypeScript cannot separate the two, because a zero-argument function is assignable to a one-argument function type. `useFonts` needs the mark only from the second argument on. Its first argument has never accepted a loader, so there is nothing to disambiguate and a bare resolver still works there. In Vue, a function origin is always the value, never a getter to call. To build an origin lazily, use a `computed`: ```ts const fonts = useFonts(computed(() => packagedFonts())); ``` `useFonts(() => packagedFonts())` cannot work: nothing distinguishes it from a resolver that ignores its request. The editor reports it rather than composing an empty result. ```ts import { defineFontResolver } from '@docx-editor.dev/core/editor'; const faceKey = (family: string, weight: number, style: string) => // Case-folded: Word matches font names that way, and so do both shipped resolvers. `${family.trim().toLowerCase()} ${weight} ${style}`; const brandFonts = defineFontResolver(async ({ families, defaultFamily, resolvedFaces }) => { const already = new Set( (resolvedFaces ?? []).map((face) => faceKey(face.family, face.weight, face.style)) ); // `defaultFamily` alongside `families`: a run that authors no font is measured in the // default face, so a resolver that ignores it never serves that run. const wanted = [defaultFamily, ...families].filter( (family) => BRAND.has(family) && // Load a family unless EVERY face of it is already covered. Skipping a partly // covered family leaves its other faces with no bytes at all. !BRAND_FACES.every((face) => already.has(faceKey(family, face.weight, face.style))) ); return { sources: await loadBrandFaces(wanted) }; }); ``` The request carries `families`, the names that document declares, and `defaultFamily`, the face a run naming no font resolves to. Both count as declared. `families` also includes faces used by `w:sym`, `SYMBOL` fields, and numbering markers. Unused numbering definitions do not add requests. Supply usable bytes for these faces to render their authored glyphs. The editor maps private-use symbols to Unicode where a mapping exists and preserves the character otherwise. Supplied faces are also available in the font picker. `resolvedFaces` lists the faces earlier origins in the same composition can already paint. It reports faces rather than families, and only faces backed by bytes, so skipping one cannot lose one. That is what makes honoring the list an optimization rather than a requirement: composition drops a duplicate face either way, so ignoring it only costs bytes. Origins resolve one after another, not in parallel, so each can be told what the ones before it covered. That costs one extra origin's latency. Order origins cheapest-first. ## Source precedence `composeFontConfiguration(base, ...fragments)` creates one immutable configuration. | Priority | Rule | | -------- | ---------------------------------------------------------------------------------- | | 1 | An explicit source beats an embedded face with the same family, weight, and style. | | 2 | An embedded face beats a substitution. | | 3 | The first fragment wins between equal sources. | | 4 | Duplicate faces are removed. | ## Handle failures Fonts never block a document from opening. Each failed face falls back independently. | Failure path | Notification | | ------------------ | -------------------- | | React root | `onFontError` | | Vue root | `@font-error` | | `createDocxEditor` | `onFontError` option | Each `EditorFontError` has a typed `code`. Known codes include `missing`, `malformed`, `overLimit`, `hashMismatch`, and `wasmUnavailable`. Read `editor.fontMeasurement()` to inspect measurement: | Field | Meaning | | ----------- | ------------------------------------------- | | `measurer` | `'shaped'` or `'fixed'` | | `resolving` | `true` while font resolution is in progress | | `producer` | Optional source identifier | `{ measurer: 'fixed', resolving: false }` means no usable font source remains. ## Serve the HarfBuzz WASM asset Webpack, Turbopack, and Vite emit the bundled WebAssembly asset automatically. Some esbuild, Bun, and library builds do not emit `new URL(..., import.meta.url)` assets. Without `harfbuzz.wasm`, the editor reports `wasmUnavailable` and uses fixed measurement. Rendering continues, but line and page breaks can differ from Word. For affected builds: 1. Copy `harfbuzz.wasm` from the installed core package into your served assets. 2. Call `setHarfBuzzWasmUrl` before you create the first editor. 3. Repeat the copy after each package upgrade. ```js import { copyFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; await copyFile( fileURLToPath(import.meta.resolve('@docx-editor.dev/core/harfbuzz.wasm')), 'public/static/harfbuzz.wasm' ); ``` ```ts import { setHarfBuzzWasmUrl } from '@docx-editor.dev/core/layout'; setHarfBuzzWasmUrl('/static/harfbuzz.wasm'); ``` The shaper reads this URL once per module instance. A later call warns and does nothing. A Web Worker has a separate module instance and needs its own call. Use a URL that your application controls. Do not build it from user input or remote configuration. Cross-origin hosting needs that origin in `connect-src`. WebAssembly needs `wasm-unsafe-eval` in `script-src`. The shaper rejects a missing, stale, or version-mismatched binary. An npm `overrides` entry for `harfbuzzjs` does not change bundled shaper code. Upgrade `@docx-editor.dev/core` instead. For engine implementation details, see [Architecture](/docs/2.x/core/architecture). ## Next steps - [React font props](/docs/2.x/react/props#fonts): Configure React roots. - [Vue font props](/docs/2.x/vue/props#fonts): Configure Vue roots. - [Word fidelity](/docs/2.x/word-fidelity): Review pagination support. --- # Footnotes and endnotes Source: https://www.docx-editor.dev/docs/2.x/guides/footnotes-and-endnotes The editor renders Word footnotes and endnotes in their document locations. You can edit note text through the same document surface. ## Insert a note Open the **Insert** menu. Select **Footnote** or **Endnote**. The editor adds a reference at the caret and creates the matching note body. It then opens that note body for editing. React and Vue use the same `insert.footnote` and `insert.endnote` chrome slots. Custom chrome can run these shared commands. ## Edit a note Select a note reference to enter its note scope. Text, lists, tables, content controls, pictures, fonts, comments, and bookmarks work inside that scope. Comments need the review module and an EigenPal Pro License. Use `useNoteScopeState()` in React or Vue to read the active note scope. `DocxEditor.NotesChrome` and `DocxEditorNotesChrome` provide packaged note controls for React and Vue. ## Convert or delete a note The note controls can convert a footnote to an endnote. They can also convert an endnote to a footnote. In edit mode, deleting a note removes its reference and note body together. The editor updates later note numbers after the operation. Suggesting mode refuses direct note deletion. Delete the reference to propose the deletion. The Editing API can enumerate, read, navigate, and delete notes. Use `document.footnotes` and `document.endnotes`. ## Placement and pagination The editor supports Word note positions such as page bottom, beneath text, section end, and document end. A footnote stays with its reference line when both fit on one page. The editor moves that line when the note cannot fit below it. A note splits only when it is taller than the available note column. Split note pages keep separate rectangles for painting and pointer input. ## Limits Note editing has these limits: - Notes inside headers and footers are not supported. - Suggesting mode tracks the inserted note reference. - Suggesting mode requires reference deletion to propose note removal. - Imported note revisions remain part of the round-trip document. ## Next steps - [Fields and cross-references](/docs/2.x/guides/fields): Review `NOTEREF` behavior. - [Tracked changes](/docs/2.x/pro/tracked-changes): Review revision support in other stories. - [Editing API compatibility](/docs/2.x/editor-api/office-js-api): Use note collections in code. --- # Headers and footers Source: https://www.docx-editor.dev/docs/2.x/guides/headers-footers Headers and footers render in their Word page positions. Double-click the header or footer area to edit it in place. ## Editing Double-click inside the header or footer band to enter edit mode. From there, editing follows the same model as the document body: click to place the caret, drag to select, double- and triple-click for word and paragraph selection, right-click menus, hyperlinks, image selection, and table row, column, and edge resize all work identically. Undo and redo cover header and footer edits. While editing, a separator bar labels the region (**Header** or **Footer**) and shows an **Options** menu: - **Insert current page number** inserts a `PAGE` field. - **Insert total page count** inserts a `NUMPAGES` field. - **Insert section page count** inserts a `SECTIONPAGES` field. - **Remove header/footer** clears the region. Press Escape or click the close action to save and leave header/footer editing. ## Page-number fields `PAGE`, `NUMPAGES`, and `SECTIONPAGES` stay as Word fields. The page view updates their values when the document reflows. `PAGE` shows the displayed page number. It honors the section start and format in `w:pgNumType`. `NUMPAGES` shows the document page count. `SECTIONPAGES` shows the page count for the current section. A "Page X of Y" footer is the two inserts with literal text between them: type the text "Page ", insert the page number, type " of " with the surrounding spaces, insert the total page count. A page-number field can carry a numeric picture switch. For example, `PAGE \# 0#` pads page 2 to `02`. The editor applies the picture to the computed header or footer value. A saved result does not replace that computed value. Pictures support digit placeholders, a grouping comma, and literal text. Other fields use separate evaluation rules. See [Fields and cross-references](/docs/2.x/guides/fields). Legacy footers can store a centered `PAGE` field in an auto-sized `w:framePr` over an empty paragraph of the same style or a centered middle-dot decoration. Both paragraphs render in one footer band. Page values are re-centered per page, and the source structure survives save. A supported fixed-width frame uses a page-relative X position and text-relative vertical centering over an empty anchor or a second `PAGE` paragraph. A leading tab can place its value outside the frame. The editor clips that value but retains all fields, paragraphs, and source ranges. Selections and pointer hits use the same clip rectangle. This support covers a simple, single-line `PAGE` frame and a single-line empty or `PAGE` anchor. Values must fit completely inside the frame or lie completely beyond it. Partial wrapping, other positions, paragraph spacing, and additional content keep the ordinary flow behavior. ## Per-section headers and footers The OOXML model attaches header and footer references to sections, and the editor implements that model: - Each section can reference `default`, `first`, and `even` header and footer parts (`w:headerReference` / `w:footerReference`). - **Different first page** (`w:titlePg`) shows the `first` variant on a section's first page. Typical use: no header on a title page. - **Different odd and even pages** (`w:evenAndOddHeaders`) alternates `default` and `even` variants, as in book-style layouts. - Sections without their own references inherit from earlier sections, matching Word's "link to previous" behavior. - A page whose variant has no reference shows no header or footer, and its body starts at the page margin. A title page in a section that references only a `default` header is the common case. Documents using any of these render and round-trip correctly; editing a header edits the specific part the page displays. ## Watermarks Word stores watermarks in header parts. The editor renders the supported unrotated [legacy VML subset](/docs/2.x/guides/images#legacy-vml-previews) there. Rotated or curved watermark templates remain opaque. Watermark insertion, editing, and headless authoring are not supported. The editor preserves the original VML or DrawingML markup and image payloads when you save. ## Next steps - [Architecture](/docs/2.x/core/architecture) for how header/footer editing shares the body's editing model - [Loading and saving](/docs/2.x/guides/loading-and-saving) for the save pipeline that round-trips these parts - [Editing API](/docs/2.x/editor-api) for editing a document from a server --- # Images and drawings Source: https://www.docx-editor.dev/docs/2.x/guides/images DrawingML pictures (`w:drawing` with `pic:pic`) lay out, paint, and round-trip through the engine. Both adapters provide insert, wrap, properties, alt text, and selection-overlay authoring. ## Supported formats | Format | Insert | Layout & paint | Round-trip | | ------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------ | ----------------------------------------- | | PNG, JPEG, GIF | Yes, `normalizeImageBytes` preflight | Full decode at authored `wp:extent` | Original media preserved unless replaced | | BMP, WebP | No insert | Full decode at authored `wp:extent` | Original media preserved | | SVG | No insert | Painted at authored `wp:extent` | Original media preserved | | TIFF, EMF, WMF | No insert | Rasterized when conversion succeeds; labeled placeholder otherwise | Preserved; no zero-click fetch | | External `r:link` / `TargetMode="External"` | No auto-load | Placeholder + preserved rel | Preserved; explicit user gesture to embed | Insert accepts PNG/JPEG/GIF only. Other payloads stay in the package and affect pagination through their authored extent. JPEG validation accepts large EXIF and ICC metadata segments before the frame header. Intrinsic dimensions account for EXIF orientation, matching the browser's decoded image. The original photo bytes and the authored drawing extent remain unchanged. BMP and WebP decode in the browser and paint like PNG or JPEG. BMP support includes top-down bitmaps and the 12-byte `BITMAPCOREHEADER`. WebP support includes lossy, lossless, and extended containers. TIFF, EMF, and WMF paint when conversion to a validated raster succeeds. The original media stays unchanged in the package through save. When conversion is unavailable, declined, or fails, the image keeps its extent and shows a labeled placeholder. SVG is painted through an ``, which puts the browser in secure static mode: scripts in the file never run and external references in it are never fetched. Its intrinsic size is read from `width`/`height`/`viewBox` for reset-to-natural-size; layout always uses the authored `wp:extent`. An SVG whose root element cannot be read renders as a placeholder rather than inferring missing dimensions. ## Reading selection state Use the canonical read model shared by snapshot and imperative API: #### React ```tsx import { useEditorState } from '@docx-editor.dev/react'; const image = useEditorState((s) => s.image); // SelectedImageState | null: widthEmu/heightEmu, crop, wrap, position, locks, resourceStatus ``` #### Vue ```ts import { useEditorState } from '@docx-editor.dev/vue'; const image = useEditorState((s) => s.image); // ShallowRef: widthEmu/heightEmu, crop, wrap, position, // locks, resourceStatus ``` ```ts editor.getSelectedImage(); // same SelectedImageState shape editor.snapshot().image; // reference-stable until selection or image fields move ``` `SelectedImageState.wrap` reports one of these Word menu targets: | Value | Text behavior | OOXML mapping | | -------------- | ----------------------------------------------- | ---------------------------- | | `inline` | Places the picture in the text line | `wp:inline` | | `square` | Wraps on both sides of a square boundary | `wrapSquare`, `bothSides` | | `squareLeft` | Wraps on the left | `wrapSquare`, `left` | | `squareRight` | Wraps on the right | `wrapSquare`, `right` | | `tight` | Wraps around the authored polygon | `wrapTight` | | `through` | Wraps through the authored polygon | `wrapThrough` | | `topAndBottom` | Keeps text above and below | `wrapTopAndBottom` | | `behind` | Places the picture behind document content | `wrapNone`, `@behindDoc="1"` | | `inFront` | Places the picture in front of document content | `wrapNone`, `@behindDoc="0"` | Images in front of or behind text can paint beyond their anchor cells. Their cell-relative position is preserved, but the cell does not crop these overlays. Page-relative images are clipped to the physical sheet, including its margins. The `resourceStatus` field reports image availability: | Status | Meaning | | -------------- | --------------------------------------------------------------------------- | | `pending` | Validation or decoding is in progress | | `ready` | Validated image bytes are ready to paint | | `unrenderable` | Validation, decoding, conversion, or format support failed | | `external` | The relationship points outside the package and is not loaded automatically | | `missing` | The relationship target is missing | ## Authoring surface Both adapters export the same parts under the same names. Default toolbar slots (contextual `image` group when a picture is selected): | Slot | Component / hook | Engine command | | ------------------ | ----------------------------------------------------------- | ------------------------------------------------- | | `image.insert` | `ImageInsertProvider`, `ImageInsertTrigger` | `executeImageCommand({ type: 'insertImage', … })` | | `image.wrap` | `ImageWrap`, `useEditorValueCommand('image.wrap')` | `setImageWrapType` via `runToolbarCommand` | | `image.altText` | `ImageAltText` | `setImageAltText` | | `image.properties` | `ImagePropertiesTrigger`, `DocxEditorImagePropertiesDialog` | `setImageProperties` | The packaged Insert menu carries an **Image** row that opens the same file picker as `ImageInsertTrigger`. Hide it or place it elsewhere through `DocxEditor.Menu.ImageInsert`. `widthPoints` and `heightPoints` are the extent to insert at most. An image that fits where the caret flows keeps its natural size; a wider or taller one scales down proportionally to its cell, column, or page content box. Read the committed extent back from `snapshot().image`. Insert preflight: #### React ```ts import { normalizeImageBytes } from '@docx-editor.dev/react'; const result = normalizeImageBytes(bytes); if (result.ok) { await editor.executeImageCommand({ type: 'insertImage', data: result.bytes, mime: result.mime, widthPoints: result.widthPoints, heightPoints: result.heightPoints, }); } ``` #### Vue ```ts import { normalizeImageBytes } from '@docx-editor.dev/vue'; const result = normalizeImageBytes(bytes); if (result.ok) { await editor.executeImageCommand({ type: 'insertImage', data: result.bytes, mime: result.mime, widthPoints: result.widthPoints, heightPoints: result.heightPoints, }); } ``` Floating pictures: select in the canvas, drag to move (`setImagePosition`), eight handles or Alt+Arrow to resize (`setImageProperties`), wrap menu for all nine choices. One undo step per completed gesture. ## Wrap modes and text reflow Wrap changes are **layout-structural**: exclusion zones feed line breaking before paint. Square/tight/through use authored polygons (with distances and `effectExtent`); top-and-bottom and square variants reflow body text accordingly. Header/footer rule: page-relative anchored letterheads do **not** inflate the header box. Body flow height still sizes the furniture band. Text distances inherit each missing side from `wp:anchor`; an explicit wrap-child value, including zero, overrides that side. When square or rectangular tight wrapping leaves no passage wide enough for the next glyph, text clears the image before continuing. The clearance participates in pagination without repeating the gap on subsequent lines. Nonrectangular contours keep their existing scanline behavior. ## Accessibility Alt text uses `@descr`, then `@title`; `@name` is never announced. A picture with neither description nor title is exposed as decorative. Hidden drawings (`@hidden`) suppress paint, hit-testing, and handles while remaining preserved on save. ## Security and external images - No network fetch on open, layout, paint, or save for external relationships. - Embedded bytes are signature-checked and dimension-capped before decode. - Hyperlinks on drawings (`a:hlinkClick`) require an explicit gesture and pass through `sanitizeHref`. - Optional explicit download-and-embed path for external targets is user-initiated only (bounded size/content-type). ## Drawing limits The editor renders brightness, contrast, grayscale, and bilevel black-and-white picture adjustments. It preserves image alpha and adjustment markup when you save. PDF export does not apply these adjustments. | Drawing content | Status | | ------------------------------------------------------- | ---------------------------------------------------- | | Solid shapes and bounded shape groups | Render geometry with sRGB or theme colors | | Charts, SmartArt, unsupported groups, and canvases | Preserve the extent and show a labeled placeholder | | Anchored text boxes | Render the story read-only and clip it to the extent | | Page fields in anchored header or footer text boxes | Evaluate per page | | Inline text boxes, linked chains, autofit, and rotation | Show a placeholder | | Standalone VML (`w:pict`) | Render the bounded read-only subset described below | | `w:object` and `w:altChunk` | Preserve as generic content with diagnostics | | Tracked image insertion and deletion | Record the change in suggesting mode | | Tracked image property edits | Stay unavailable in suggesting mode | | Artistic effects (`a:effectLst`) | Do not render; authored markup round-trips | ## Find text in text boxes **Find** searches anchored text boxes in the body, headers, and footers. Selecting a match selects its text box; the content remains read-only. Inline text boxes and text boxes in footnotes or endnotes are not searched. Use `useDocumentSearch` to add Find navigation to a custom [React](/docs/2.x/react/hooks#usedocumentsearch) or [Vue](/docs/2.x/vue/composables#usedocumentsearch) interface. ## Legacy VML previews Standalone `w:pict` can render unrotated embedded photos and bounded groups of photos, solid rectangles/ellipses, straight line segments with arrows, and straight fit-to-box WordArt (`_x0000_t136`). Group-local coordinates, picture crops, supported text wrapping, and source order are retained. WordArt uses the authored font when available on the host; curved or rotated WordArt is not supported. Previews are read-only. Saving preserves the original VML, media, and relationships without adding generated SVG parts or converting the source to DrawingML. Deleting another picture preserves shared photo relationships. The editor does not partially render unsupported, oversized, or clipped groups. Custom templates, text boxes, curves, image effects, rotation, and unsupported group members prevent a group preview. Only recognized standard shape templates are supported. When a document supplies DrawingML and VML alternatives, the editor displays only the selected alternative. Standalone photos reuse validated image resources and retain their authored crop. Groups, WordArt, and color-key previews use bounded static SVG resources. Embedded members still pass the normal image validation and decode limits; an external or missing member never triggers a fetch or a misleading partial group preview. ## Next steps - [Toolbar customization](/docs/2.x/guides/toolbar): override the toolbar's `ImageWrap` part and its siblings - [Word fidelity matrix](/docs/2.x/word-fidelity): live support claims - [Headers and footers](/docs/2.x/guides/headers-footers): letterhead anchors in page bands --- # Loading and saving Source: https://www.docx-editor.dev/docs/2.x/guides/loading-and-saving The editor accepts `.docx` data and serializes the edited document to `.docx`. Load and save run in the browser. The editor does not upload or convert through a service. ## Input formats The editor takes the document through the `document` prop. The most common value is `Uint8Array` DOCX bytes: #### React ```tsx ``` #### Vue ```vue ``` ### Starting empty To open the editor on an empty page, pass `'blank'`. It is Word's blank template, with the same Calibri 11pt defaults a new document in Word has. It also carries Word's built-in style gallery: Heading 1 through Heading 9, Title, Subtitle, Quote, No Spacing, and List Paragraph. #### React ```tsx ``` #### Vue ```vue ``` Omitting `document` differs from `'blank'`. It means no document at all, so the editor shows its loading screen and every control stays disabled. Use `undefined` only while your own fetch is still running. For a **File > New** command that a user can run more than once, call `blankDocumentBytes()` instead. `'blank'` is a constant, so the editor treats a second `'blank'` as the same document and keeps what the user typed. Fresh bytes replace it: #### React ```tsx import { blankDocumentBytes } from '@docx-editor.dev/core/editor'; ; ``` #### Vue ```vue ``` Call `blankDocumentBytes()` inside an event handler or into state, never inline in the `document` prop. It returns a new array each time, so an inline call rebuilds the editor on every render. ### From a URL #### React ```tsx import { useEffect, useState } from 'react'; import { DocxEditor } from '@docx-editor.dev/react'; export function Editor({ url }: { url: string }) { const [doc, setDoc] = useState(); useEffect(() => { let cancelled = false; // ignore stale responses if url changes fetch(url) .then((r) => r.arrayBuffer()) .then((buffer) => { if (!cancelled) setDoc(new Uint8Array(buffer)); }); return () => { cancelled = true; }; }, [url]); return ; } ``` #### Vue ```vue ``` `useDocxSource()` fetches the bytes and tracks the request, so a changing URL never applies a stale response. ### From a file input Read the selected file into bytes, then pass those bytes through `document`: #### React ```tsx import { useState } from 'react'; import { DocxEditor } from '@docx-editor.dev/react'; export function FileEditor() { const [doc, setDoc] = useState(); return ( <> { const file = e.target.files?.[0]; if (!file) return; setDoc(new Uint8Array(await file.arrayBuffer())); }} /> {doc && } ); } ``` #### Vue ```vue ``` `useDocxSource()` takes a URL string, a `URL`, a `Uint8Array`, or an `ArrayBuffer`. For a picked file, read the bytes first, as this sample does. ### Swapping documents at runtime To replace the document without remounting the component, use the ref: #### React ```tsx const ref = useRef(null); ref.current?.load(nextBytes); ``` #### Vue ```ts const editorRef = ref(null); editorRef.value?.load(nextBytes); ``` ## Saving Use one of these save paths: - `save()` on the ref returns `Promise` - The packaged File → Save action, which you override with `onSave` in React and the `@save` emit in Vue #### React ```tsx void persist()} /> ``` #### Vue ```vue ``` `save()` on the ref resolves `null` when no editor is mounted, so guard the result. `Editor.save()` returns an `ArrayBuffer` on success and rejects when saving fails. ### Field result refresh Save validates pending protected form input and applies the field's format using the locale active when you entered the value. Invalid input rejects with code `invalidArgs`. The editor keeps the input and does not open an alert. Collaborative sessions reject saves that require form-field formatting. Save updates stale, calibrated `REF` and `NOTEREF` results in the body, footnotes, and endnotes. The editor commits all updated parts as one undo step. Locked fields and unsafe result structures keep their saved values. View mode, read-only sessions, and collaborative sessions do not rewrite field results. See [Fields and cross-references](/docs/2.x/guides/fields) for supported switches and other limits. ## Download helper The serialized buffer downloads like any other binary. This helper is the same in both adapters, because both export the `DocxEditorRef` type: ```ts async function downloadDocx(ref: DocxEditorRef, fileName: string) { const buf = await ref.save(); if (!buf) return; const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName.endsWith('.docx') ? fileName : `${fileName}.docx`; a.click(); URL.revokeObjectURL(url); } ``` ## Autosave Debounce the save call when the document reports a change: #### React ```tsx import { useRef } from 'react'; import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/react'; export function AutosaveEditor({ docId, bytes }: { docId: string; bytes: Uint8Array }) { const ref = useRef(null); const timer = useRef(null); const onChange = () => { if (timer.current) window.clearTimeout(timer.current); timer.current = window.setTimeout(async () => { try { const buf = await ref.current?.save(); if (!buf) return; await fetch(`/api/documents/${docId}`, { method: 'PUT', body: buf }); } catch (error) { console.error('Autosave failed', error); } }, 1500); }; return ; } ``` #### Vue ```vue ``` Pick a debounce window that matches your backend's write tolerance. Use a 1–2 second debounce unless your API requires another interval. ## Next steps - [React props](/docs/2.x/react/props) and [Vue props](/docs/2.x/vue/props) for `document`, the save action, and the shared ref - [Editing API](/docs/2.x/editor-api) to read and write a document without mounting an editor - [React examples](/docs/2.x/react/examples) and [Vue examples](/docs/2.x/vue/examples) for more loading, saving, and editor state patterns --- # Toolbar Source: https://www.docx-editor.dev/docs/2.x/guides/toolbar The packaged host has two levels of chrome: a title bar (`title`, the title bar slots, and `menu`) and a formatting bar (the `Toolbar` part). Both the packaged host and the lower-level compounds live on the adapter package root. ## Use the packaged chrome Use the packaged host for the default chrome: #### React ```tsx } /> ``` #### Vue ```vue ``` Use these root props to configure the packaged frame: #### React | Prop | Type | Description | | -------------------------------------------- | --------------------------------------- | -------------------------------------------------- | | `title` | `string` | Document name shown in the title bar. | | `onTitleChange` | `(title: string) => void` | Makes the title editable. | | `renderTitleBarLeft` / `renderTitleBarRight` | `() => ReactNode` | Host-owned title bar slots. | | `menu` | `boolean \| DocxEditorMenuProps` | Toggle or customize the packaged menu row. | | `chrome` | `boolean` | Set `false` to remove the packaged frame entirely. | | `navigation` | `boolean` | Toggle the packaged navigation pane. | | `hyperlinkPopup` | `boolean` | Toggle the packaged link popover. | | `contextMenu` | `boolean \| DocxEditorContextMenuProps` | Toggle or customize the packaged context menu. | #### Vue | API | Type | Description | | ---------------------------------- | --------------------------------------- | -------------------------------------------------- | | `title` | `string` | Document name shown in the title bar. | | `@title-change` | `(title: string) => void` | Makes the title editable. | | `#titleBarLeft` / `#titleBarRight` | slot | Host-owned title bar slots. | | `menu` | `boolean \| DocxEditorMenuProps` | Toggle or customize the packaged menu row. | | `chrome` | `boolean` | Set `false` to remove the packaged frame entirely. | | `navigation` | `boolean` | Toggle the packaged navigation pane. | | `hyperlinkPopup` | `boolean` | Toggle the packaged link popover. | | `contextMenu` | `boolean \| DocxEditorContextMenuProps` | Toggle or customize the packaged context menu. | ## Use the provider primitives When you want your own frame, use the same provider primitives the packaged host uses internally: #### React ```tsx import { DocxEditor } from '@docx-editor.dev/react'; function MyChrome({ bytes }: { bytes: Uint8Array }) { return ( ); } ``` #### Vue ```vue ``` The root owns the editor instance, the viewport is the scroll container, and the content part is the painted page surface. The other compounds layer on top of that same provider. ## Add root-level command buttons For small custom controls, call the shared command API from the package root: #### React ```tsx import { useEditorCommand } from '@docx-editor.dev/react'; function BoldButton() { const bold = useEditorCommand('text.bold'); return ( ); } ``` #### Vue ```vue ``` `useEditorCommand` returns computed refs on a plain object, so read `isEnabled`, `isActive`, and `disabledReason` with `.value`. See the [chrome slot reference](/docs/2.x/guides/chrome-slots) for every slot, its packaged surface, and the matching React and Vue toolbar part. ## Format painter The `format.painter` slot copies the formatting at the selection and applies it somewhere else. The text never moves. - Click the control once to arm it for a single application, then select the text to format. - Double-click the control to keep it on for repeated applications, and press `Esc` to release it. - Press `Ctrl+Alt+C` to copy the formatting and `Ctrl+Alt+V` to apply it. On macOS, use `Command` in place of `Ctrl`. On macOS, browsers bind `Command+Option+C` to their own element inspector, and a web page can't override a browser shortcut. To copy formatting there, use the control or the right-click menu. `Command+Option+V` applies the formatting as expected. On Windows and Linux keyboard layouts that put characters on the `AltGr` level of `C` — for example Polish — `Ctrl+Alt+C` types that character instead. Use the control or the right-click menu. - Right-click the document and choose **Copy formatting** or **Paste formatting**. What the painter copies follows the selection. A selection inside one paragraph copies character formatting: font, size, color, and the character marks. A selection that covers the paragraph mark also copies the paragraph style, alignment, spacing, and indents. The painter reads the formatting the reader sees, not only what the text states directly. Copying from a paragraph that takes its face from a style carries that face to the target. Three things stay on the target: paragraph borders (`w:pBdr`), a run's character style (`w:rStyle`), and character shading (`w:shd` on a run). To drive the painter from your own chrome, use the `copyFormatting` and `pasteFormatting` commands. `pasteFormatting` reports a disabled reason until something is copied. ## Table editing When the caret or a rectangular cell selection is inside a table, the formatting toolbar shows contextual table controls: - **Resize**: hover a column divider or the table's right edge; drag to commit Word-compatible twip widths (inner divider resize preserves total table width; outer-right resize changes table width). - **Insert rows/columns**: hover the row band or column header band and click the `+` control, or use the table rows on the right-click menu. - **Borders and fill**: with one or more cells selected, choose a border target, style, width, and color, then pick a cell fill (clear fill removes direct shading). These controls target the **innermost** nested table under the pointer or selection. Merge and split remain unsupported; tables with merged cells refuse column resize/insert/delete with an explicit disabled reason. The [Igloo demo](https://igloo.docx-editor.dev/) shows a custom toolbar order, custom icons, and two host actions. See the [Igloo example source](https://github.com/eigenpal/docx-editor/tree/main/examples/igloo). ## Next steps - [Chrome slot reference](/docs/2.x/guides/chrome-slots) - [React package overview](/docs/2.x/react) and [Vue package overview](/docs/2.x/vue) - [React props](/docs/2.x/react/props) and [Vue props](/docs/2.x/vue/props) - [React examples](/docs/2.x/react/examples) and [Vue examples](/docs/2.x/vue/examples) --- # Zoom and fit to screen Source: https://www.docx-editor.dev/docs/2.x/guides/zoom The editor fits the page to its container by default. A container wide enough for the page renders at 100%. A narrower container shrinks the document instead of creating a horizontal scrollbar. ## The two modes Zoom has a value and a mode. The value is the scale the pages paint at; the mode is where that value comes from. | Mode | Behavior | | ----------------------------------- | ------------------------------------------------------------------------------------------------- | | `'auto'` (default) | Fit the page width, between 50% and 100%. Shrinks a page that does not fit, leaves one that does. | | `{ type: 'fit', fit: 'pageWidth' }` | Fit the page width in both directions, so a wide window magnifies the page. | | `{ type: 'fixed' }` | Hold whatever `zoom` is set to. This keeps a fixed scale (the behavior before fit modes). | #### React ```tsx {/* auto */} ``` #### Vue ```vue ``` Passing `zoom` on its own also means fixed, so an app that pinned a scale keeps it. Fit mode updates when the container size changes. Resizing the window, opening the comments pane, or docking the navigation pane changes the space beside the page. The engine updates the scale without additional host code. Setting a level ends the fit. A fixed zoom replaces fit mode. Container resize must not override an explicit scale. ## Driving it yourself `useZoom` returns zoom state and controls in one hook. #### React ```tsx import { useZoom } from '@docx-editor.dev/react'; function ZoomControl() { const { zoom, isFit, auto, zoomIn, zoomOut, canZoomIn, canZoomOut } = useZoom(); return (
{Math.round(zoom * 100)}%
); } ``` #### Vue ```vue ``` The Vue composable returns computed refs. Destructured bindings unwrap in the template, so read them with `.value` only in script code. | Member | What it does | | ------------------------- | ----------------------------------------------------------------------------------------- | | `zoom` | The resolved scale. `1` is 100%. | | `mode`, `isFit` | Where the scale came from. Render a control's selected state from these, not from `zoom`. | | `setZoom(n)` | A fixed scale. Leaves any fit. | | `setMode(m)` | `'auto'`, a fit, or `{ type: 'fixed' }`. | | `auto()`, `fitToWidth()` | The two fits, by name. | | `reset()` | Reset to fixed 100% zoom. | | `zoomIn()`, `zoomOut()` | Move to the next preset in `levels`. | | `canZoomIn`, `canZoomOut` | Whether a higher or lower preset remains. | Render the selected state from `mode`, not from `zoom`. A menu that ticks the level matching the percentage marks "75%" as selected while fit mode still owns the scale. The packaged toolbar's zoom control already does all of this: **Automatic** and **Fit width** sit above the preset levels, and the tick follows the mode rather than the percentage. It ticks those two modes and the preset levels — the modes the menu can select. A fit with bounds of your own is not one of them, so the menu shows the resolved percentage with nothing ticked, rather than marking a preset that would replace custom fit bounds on click. ## Comments, and narrow screens The comments rail reserves a gutter beside the page. Under a fit that gutter comes out of the document's width, so opening comments shrinks the page rather than pushing it off screen: the two sit side by side and both stay readable. Below a container width, reserving the rail width no longer keeps the page readable. The rail takes a fixed 316px whether or not the container can spare it. On a phone with comments open, fit would shrink the page below a readable width. So `'auto'` has a floor of 50%: below that the page keeps a legible size and the container scrolls sideways, which matches the usual layout response when content exceeds the container. Set your own floor if 50% is not where you want it: #### React ```tsx ``` #### Vue ```vue ``` ## Without an adapter Zoom APIs are on the engine. Every host calls the same methods: ```ts editor.setZoomMode('auto'); editor.getZoom(); // 0.79 on a container too narrow for the page editor.getZoomMode(); // { type: 'fit', fit: 'pageWidth', minZoom: 0.5, maxZoom: 1 } editor.setZoom(1.5); // leaves the fit editor.getZoomMode(); // { type: 'fixed' } ``` `snapshot()` carries `zoom` and `zoomMode`, and both fire on `selectionChange` — including when the editor refits itself after a resize, so a subscriber never shows a stale percentage. --- # Contributing a translation Source: https://www.docx-editor.dev/docs/2.x/i18n/contributing The editor UI ships in ten languages. To add another language, add one JSON file in the [monorepo](https://github.com/eigenpal/docx-editor). ## How locales work `packages/i18n/en.json` is the source of truth. Every other locale mirrors its shape; a `null` value falls back to the English string at runtime, and a missing key fails CI. Locales publish as named exports and per-locale subpaths of `@docx-editor.dev/i18n`. ## Add a language ```bash git clone https://github.com/eigenpal/docx-editor && cd docx-editor bun install bun run i18n:new # scaffolds packages/i18n/.json with nulls # fill in the strings, then bun run i18n:status # shows remaining nulls per locale bun run i18n:validate # CI runs this; missing keys fail ``` Use a BCP 47 code (`pl`, `pt-BR`, `zh-CN`). Open a PR with the filled JSON. Partial translations are valid: anything left `null` renders in English, so a locale can ship incomplete and improve over time. ## Fixing an existing locale Edit the string in `packages/i18n/.json`, run `bun run i18n:validate`, and open a PR. No code changes needed. ## Next steps - [i18n package](/docs/2.x/i18n): wiring locales into the editor - [i18n API reference](/docs/2.x/api/i18n): every locale export - [Open a PR](https://github.com/eigenpal/docx-editor/pulls) --- # 2.x/i18n/index Source: https://www.docx-editor.dev/docs/2.x/i18n/index Locale data for the editor UI. Both adapters consume it through the `i18n` prop. ```bash npm install @docx-editor.dev/i18n ``` Add this package as a direct dependency when importing its catalogs, including for the editor's `i18n` prop. ## UI language and date input `i18n` supplies UI translations; `locale` selects regional date input and generated document labels. Neither setting infers the other. UI strings default to English or an inherited catalog; `locale` defaults to `en-US`. ```tsx import { DocxEditor } from '@docx-editor.dev/react'; import { pl } from '@docx-editor.dev/i18n'; // English UI, Polish dates (with no inherited catalog) ; // Polish UI, Polish dates ; ``` In Vue, use `locale="pl-PL"` and `:i18n="pl"` on ``. For composed editors, wrap Root and its chrome in `LocaleProvider`; Root has no `i18n` prop. See the [React](/docs/2.x/react/composition#ui-language-and-date-input) and [Vue](/docs/2.x/vue/composition#ui-language-and-date-input) examples. See [form fields](/docs/2.x/guides/fields) for supported date input. ## Available locales | Code | Language | Named export | | ------- | -------------------- | ------------ | | `en` | English | `en` | | `pl` | Polish | `pl` | | `de` | German | `de` | | `fr` | French | `fr` | | `pt-BR` | Brazilian Portuguese | `ptBR` | | `he` | Hebrew | `he` | | `hi` | Hindi | `hi` | | `id` | Indonesian | `id` | | `tr` | Turkish | `tr` | | `zh-CN` | Chinese (Simplified) | `zhCN` | Two import shapes are supported: - **Named exports off the root** (`import { pl } from '@docx-editor.dev/i18n'`). Use this when you ship a small static list of locales. Hyphenated codes (`pt-BR`, `zh-CN`) become camelCase exports (`ptBR`, `zhCN`). - **Per-locale subpath imports** (`import pl from '@docx-editor.dev/i18n/pl'`). Use this when you dynamically load locales; the per-locale subpath code-splits so users only download the language they need. Verify what your installed version ships: ```bash ls node_modules/@docx-editor.dev/i18n/ ``` ## Wiring into the editor Pass the catalog to `i18n`. These examples change UI language only; date input keeps the default `en-US` conventions. #### React ```tsx import { DocxEditor } from '@docx-editor.dev/react'; import { pl } from '@docx-editor.dev/i18n'; ; ``` #### Vue ```vue ``` For several editors, or for chrome parts you compose yourself, put the catalog in context once instead: #### React ```tsx import { DocxEditor, LocaleProvider } from '@docx-editor.dev/react'; ; ``` #### Vue ```vue ``` In both cases, the catalog merges over English, so a locale that has not translated every key falls back per key rather than per language. Providers nest: an inner one, or an `i18n` prop under an outer provider, overrides only the keys it names. Chrome you write from scratch reads the same catalog through `useTranslation()`: #### React ```tsx import { useTranslation } from '@docx-editor.dev/react'; const { t } = useTranslation(); ; ``` #### Vue ```vue ``` ## Next steps - [Contributing a translation](/docs/2.x/i18n/contributing): add or improve a locale with one JSON file - [i18n API reference](/docs/2.x/api/i18n) - [React API reference](/docs/2.x/api/react) and [Vue API reference](/docs/2.x/api/vue) --- # 2.x/index Source: https://www.docx-editor.dev/docs/2.x/index `docx-editor` is a what-you-see-is-what-you-get (WYSIWYG) DOCX editor for React and Vue 3. It parses Office Open XML (OOXML), renders paginated pages, and saves the edited state as a DOCX file. Browser editing needs no upload or conversion service. Install an adapter, load a DOCX file, and save the result. Build an interface with public components and hooks. Check feature support, round-trip behavior, security, and limits. Define tools a model can call to read, draft, and redline a DOCX. ## Packages | Package | Purpose | License | | ----------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------- | | [`@docx-editor.dev/react`](/docs/2.x/react) | React adapter and composition hooks | Apache 2.0 | | [`@docx-editor.dev/vue`](/docs/2.x/vue) | Vue 3 adapter and composables | Apache 2.0 | | [`@docx-editor.dev/core`](/docs/2.x/core) | Framework-independent OOXML, document, layout, paint, and editor engine | Apache 2.0 | | [`@docx-editor.dev/editor-api`](/docs/2.x/editor-api) | Browser and server automation through a documented Office.js-compatible subset | EigenPal Pro License | | [`@docx-editor.dev/pro`](/docs/2.x/pro) | Tracked changes, comments, and custom nodes | EigenPal Pro License | | [`@docx-editor.dev/i18n`](/docs/2.x/i18n) | Shared interface translations | Apache 2.0 | | [`@docx-editor.dev/fonts`](/docs/2.x/guides/fonts) | Open-licensed substitutes for common Word fonts | Apache-2.0 AND OFL-1.1 AND LicenseRef-GUST-Font-License | The Pro packages are licensed under the EigenPal Pro License, and you can compare and buy license and support levels on the [pricing page](https://www.docx-editor.dev/pricing). ## Select packages | Goal | Packages | | ---------------------------------------------------------------------- | -------------------------------------------------------------- | | React editor | `@docx-editor.dev/react` and `@docx-editor.dev/core` | | Vue editor | `@docx-editor.dev/vue` and `@docx-editor.dev/core` | | Tracked changes, comments, or custom nodes | Add `@docx-editor.dev/pro` | | Server-side editing through the documented Office.js-compatible subset | `@docx-editor.dev/editor-api` and `@docx-editor.dev/core` | | Browser automation through the same subset | Add `@docx-editor.dev/editor-api` and use its `/browser` entry | Install the React editor: ```bash npm install @docx-editor.dev/react @docx-editor.dev/core ``` For Vue, replace `@docx-editor.dev/react` with `@docx-editor.dev/vue`. ## Document capabilities | Area | Examples | | ----------- | ----------------------------------------------------------------------- | | Text | Formatting, fonts, theme colors, paragraph styles, and character styles | | Structure | Tables, lists, numbering, footnotes, endnotes, and content controls | | Page layout | Sections, columns, headers, footers, margins, and pagination | | Content | Images, hyperlinks, bookmarks, and fields | | Review | Tracked changes and comments with `@docx-editor.dev/pro` | Support differs by feature and operation. See [Word fidelity](/docs/2.x/word-fidelity) for editing, rendering, and round-trip status. Use the [live demo](https://docx-editor.dev/editor) to test a document. ## Round-trip behavior Saving preserves untouched package content and unsupported OOXML. | Content | Behavior | | -------------------------------------- | ----------------------------------------- | | Modeled document structures | Uses typed nodes for layout and editing. | | Unsupported or misplaced XML | Keeps generic nodes in the document tree. | | Media, fonts, macros, and VBA projects | Copies package payloads through save. | | Custom XML and add-in markup | Preserves structural content. | For the preservation contract and test method, see [Word fidelity](/docs/2.x/word-fidelity). For engine details, see [Architecture](/docs/2.x/core/architecture). ## Next steps - [Quickstart](/docs/2.x/quickstart): Load, edit, and save a DOCX file. - [Build a DOCX agent](/docs/2.x/build-a-docx-agent): Define document tools for a model. - [Installation](/docs/2.x/installation): Configure supported frameworks. - [React composition](/docs/2.x/react/composition): Build a React interface. - [Vue composition](/docs/2.x/vue/composition): Build a Vue interface. --- # Installation Source: https://www.docx-editor.dev/docs/2.x/installation Use one of these supported framework versions: - React `^18 || ^19` - Vue `^3.3` ## Node version To run the engine outside a browser, use Node `^20.16.0 || >=22.3.0`. Anything that measures text needs the text shaper. The shaper reaches Node builtins through `process.getBuiltinModule`, which arrived in Node 20.16.0 and 22.3.0. On an earlier version the shaper does not start. The engine then reports the Node version as the cause, rather than a missing binary. This applies to server-side rendering, headless automation with `@docx-editor.dev/editor-api`, and build-time rendering. A browser-only app is unaffected. The floor matters more than it looks. Measurement decides where lines wrap and pages break, so a shaper that cannot start is not a degraded mode. It changes your page count. ## Pick a package Each adapter declares the engine as a peer dependency. Install the engine and one adapter. Use these commands to install the packages that your app needs: #### React ```bash npm install @docx-editor.dev/react @docx-editor.dev/core ``` #### Vue ```bash npm install @docx-editor.dev/vue @docx-editor.dev/core ``` The other packages are the same for both adapters: ```bash # Tracked changes, comments, custom nodes # EigenPal Pro License: https://www.docx-editor.dev/pricing npm install @docx-editor.dev/pro # Office.js-compatible editing API, on a server or with an active editor instance # EigenPal Pro License: https://www.docx-editor.dev/pricing npm install @docx-editor.dev/editor-api @docx-editor.dev/core # Open-licensed substitutes for common Word fonts (optional) npm install @docx-editor.dev/fonts ``` ## Mount the editor Import the component and the stylesheet once. This example creates an empty document: #### React ```tsx import { DocxEditor } from '@docx-editor.dev/react'; import '@docx-editor.dev/core/styles/editor.css'; export default function App() { return (
); } ``` #### Vue ```vue ``` The Vue stylesheet imports the core stylesheet that React uses. Your app does not need Tailwind or an icon font. `document` accepts an `ArrayBuffer`, `Uint8Array`, `DocumentHandle`, or `'blank'`. Use `'blank'` for an empty document. If you omit `document`, the editor stays idle until you pass document bytes. Meet these layout requirements in both adapters: - Import the stylesheet to style the editor controls. - You do not need Tailwind because the precompiled CSS uses the `.docx-editor` scope. - Give the parent element a height because `` fills its parent. - A parent without a height collapses, so the editor does not appear. ## Use server-rendered frameworks The editor uses the Document Object Model (DOM) to measure text when it mounts. You must render the editor on the client. Vite apps need no extra boundary. Server-side rendering (SSR) frameworks need a client-only boundary. Use the guide for your framework: ## Fonts The editor uses font bytes to measure line wraps and page breaks. It loads embedded document fonts without configuration. Some documents reference Word default fonts without embedding them. For these documents, `@docx-editor.dev/fonts` supplies open-licensed substitutes. Five families match advance widths. The Century Gothic substitute stays within 1% in the package fidelity check. `packagedFonts()` supplies them per document: the editor calls it after parsing with the families that file declares. It loads a family when the document names it, or when that family is the document's default face, so a document pays for what it declares instead of all 20 eager faces. No request leaves your origin. The default face counts because a run that names no font still has to be measured in one. That face is Calibri, so Carlito loads for every document. #### React ```tsx import { DocxEditor, useFonts } from '@docx-editor.dev/react'; import { packagedFonts } from '@docx-editor.dev/fonts'; function Editor({ bytes }: { bytes: Uint8Array }) { const fonts = useFonts(packagedFonts()); return ; } ``` #### Vue ```vue ``` `packagedFonts()` resolves after the document is parsed, so the first layout uses fixed measurement and the editor re-paginates when the faces arrive. Edits made in between survive that; the undo history behind them does not. For a document that must paginate correctly on the first pass, use `defaultFonts()` instead. For more information, see [Fonts and measurement](/docs/2.x/guides/fonts#choose-between-lazy-and-eager-loading). ## Edit documents without a browser Use [`@docx-editor.dev/editor-api`](/docs/2.x/editor-api) to edit a `.docx` without a browser. You can use it on a server, in a worker, or in a script. The package opens document bytes and uses an Office.js-compatible batching object model. It saves your changes as document bytes. ## Next steps - Follow the [quickstart](/docs/2.x/quickstart) to load, edit, and save a `.docx`. - Read [React composition](/docs/2.x/react/composition) to build custom editor controls. - Read [Vue composition](/docs/2.x/vue/composition) to compose the editor with Vue components. - Review [Word fidelity](/docs/2.x/word-fidelity) for feature support and round-trip behavior. - Review [React props](/docs/2.x/react/props) and the [React API reference](/docs/2.x/api/react). --- # DOCX collaboration reference Source: https://www.docx-editor.dev/docs/2.x/pro/collaboration This reference documents the complete configuration and API behavior for real-time DOCX collaboration with the Pro package. It covers providers, room lifecycles, presence, recovery, and resource limits. For a basic WebRTC setup, use the [real-time collaboration quickstart](/docs/2.x/collaboration). The Pro module replicates text, structure, formatting, tables, headers, footers, notes, drawings, relationships, and embedded parts. Peers also share presence and remote selections across paragraphs. Collaboration requires the collaboration module from [`@docx-editor.dev/pro`](/docs/2.x/pro). Without that module, the editor does not attach a replica. Local undo remains active. `snapshot().collaborationStatus` is `'inactive'`. ```mermaid flowchart LR accTitle: Collaboration between two document editors accDescr: Each editor updates a local document and Yjs document. A provider synchronizes both Yjs documents. subgraph A[Peer A] EA[Editor] <--> DA[Local document] DA <--> YA[Yjs document] end YA <--> T[WebRTC, Hocuspocus, or custom provider] T <--> YB[Yjs document] subgraph B[Peer B] YB <--> DB[Local document] DB <--> EB[Editor] end ``` ## Prerequisites Install the Pro package, Yjs, and the provider for your transport. For WebRTC, run: ```bash npm install @docx-editor.dev/pro yjs y-webrtc ``` For Hocuspocus, run: ```bash npm install @docx-editor.dev/pro yjs @hocuspocus/provider ``` `yjs`, `y-webrtc`, and `@hocuspocus/provider` are optional Pro peer dependencies. Pro installs `y-protocols`. ## Connect with `useWebrtcCollaboration` `useWebrtcCollaboration` owns the WebRTC room. It creates the replica and builds `modules`. It destroys the room when you leave. The WebRTC subpath keeps the network provider out of review-only bundles. Import `@docx-editor.dev/pro/react/webrtc` or `@docx-editor.dev/pro/vue/webrtc`. Pass `room` to connect when the component mounts. Call `leave` to end the session. Do not mount the editor while `pending` is true. The hook returns an object. Its `session` field contains a `CollaborationSession`. The session exposes identity, status, presence, and undo. It does not expose `attach` or `gateOperations`. `CollaborationSession`, `CollaborationStatus`, and `CollaborationFailure` are exported types from `@docx-editor.dev/pro/react` and `@docx-editor.dev/pro/vue`. Room failures make `connect` and `rejoin` resolve to a `CollaborationFailure`. Success resolves to `null`. `rejoin` still throws if you call it before a connection attempt. The hook also puts room failures in `error` for rendering: ```tsx const failure = await connect(options); if (failure?.code === 'initialization-timeout') { // No room server answered. } ``` Branch on `failure.code` or `error.code` instead of matching message strings. `error` reports an initial connection failure or a session failure after connection. An expired token, `concurrent-seed`, or digest mismatch can occur after a successful join. Always handle `error`, even when `document` is not `null`. The `` host accepts `document` and `modules`. You do not need `DocxEditor.Root` for collaboration. #### React ```tsx import { useRef } from 'react'; import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/react'; import { useWebrtcCollaboration } from '@docx-editor.dev/pro/react/webrtc'; type CollaborativeEditorProps = { roomId: string; bytes: Uint8Array; actorId: string; name: string; }; export function CollaborativeEditor({ roomId, bytes, actorId, name }: CollaborativeEditorProps) { const editorRef = useRef(null); const { document, modules, session, pending, error, leave } = useWebrtcCollaboration({ room: { roomId, identity: { actorId, name }, bootstrap: { kind: 'create-or-join', document: bytes }, }, }); async function leaveRoom() { // `leave` requires current bytes. The hook cannot read them. const saved = await editorRef.current?.save(); if (saved) { leave(new Uint8Array(saved)); } } if (error) return

{error.detail ?? error.code}

; if (pending || !document) return

Connecting…

; return ( <> ); } ``` #### Vue ```vue ``` With `create-or-join`, every peer uses the same room and bootstrap settings. Each peer must use a unique `actorId`. The first peer seeds the empty room from `document`. Later peers join the existing room. Use `create` or `join` when each host already knows its role. Two isolated peers can seed the same room at the same time. This creates a room that clients cannot repair. Each replica reports `concurrent-seed` and stops. Create a new room from saved bytes. Omit `room` when the user chooses to share or join later. Then call `connect` with the same options. `connect` and `leave` keep stable identities across renders. The editor reads `modules` at construction time. Set `key` to the session `sessionId`, as shown in the examples. Without `key`, a connection after mount does not register the collaboration module. The editor continues local editing, and no changes replicate. The session logs a warning if you create it without attaching it. The hook also returns `ydoc` and `provider`. Both values are `null` while the connection is pending. Use them for persistence or direct provider access. `rejoin(bytes)` leaves with saved bytes and rejoins the same room after an `error` status. The default Pro entry exports `collaborationModule` without a network provider. Import `@docx-editor.dev/pro/collaboration` for Yjs factories. Import `@docx-editor.dev/pro/collaboration/webrtc` for room helpers such as `createCollaborationRoomId`. ## Mount the editor A collaborative root needs three values from the room. `key` remounts the editor for a new session. `document` contains the room bytes. `modules` contains the hook modules. Incorrect values can create a working editor that does not replicate edits. `DocxEditorCollaborationRoot` sets all three values. Import it with `DocxEditorCollaboration` from the framework entry. It also uses the room identity name as `author`. Set `author` to override this value. Other `DocxEditor.Root` props pass through. #### React ```tsx import { DocxEditor } from '@docx-editor.dev/react'; import { DocxEditorCollaboration, DocxEditorCollaborationRoot } from '@docx-editor.dev/pro/react'; import { useHocuspocusCollaboration, type UseHocuspocusCollaborationConnectOptions, } from '@docx-editor.dev/pro/react/hocuspocus'; interface CollaborativeEditorProps { room: UseHocuspocusCollaborationConnectOptions; } export function CollaborativeEditor({ room }: CollaborativeEditorProps) { const collaboration = useHocuspocusCollaboration({ room }); if (collaboration.error) { return

{collaboration.error.detail ?? collaboration.error.code}

; } return ( Connecting…

}>
); } ``` React renders the `fallback` prop whenever `document` is `null`. The root cannot distinguish a pending connection from a failure. Handle `error` before you render the root. #### Vue ```vue ``` Vue composables return refs. Wrap the return with `reactive` before you pass it to `DocxEditorCollaborationRoot`. Vue renders the named `fallback` slot whenever `document` is `null`. Without that slot, it renders nothing. Handle `error` before you render the root. Use `DocxEditor.Root` directly when one page mounts two rooms, or when the bytes come from somewhere the component cannot see: ```tsx if (collaboration.error) { return

{collaboration.error.detail ?? collaboration.error.code}

; } if (!collaboration.document) { return

Connecting…

; } return ( ); ``` ## Show status and participants `useCollaborationStatus()` returns these fields: - `status` gives the session state, or `inactive` when no session exists. - `reason` gives the failure for the present state. It clears after recovery. - `lastFailure` keeps the latest terminal error after the status changes. - `live` is true when edits made now reach the room. - `diverged` is true when `status` is `error` or `destroyed`. Call `rejoin` to recover a room that the hook connected before. - `attached` is true when an editor has attached its document port. A live, unattached session usually means the editor did not remount for the session. `useCollaborationParticipants()` returns the room participants. Both hooks accept an optional `session`. Omit it to read the session from the editor provider. Pass it for a room that the editor does not own. The presence parts use the same rule. `session` is optional on `DocxEditorCollaboration.Avatars` and `DocxEditorCollaboration.CaretLabels`. All of these are available from `@docx-editor.dev/pro/react` and `@docx-editor.dev/pro/vue`. If you own the Yjs resources, use `useDocumentCollaboration` from the same entries. It provides the same `connect` and `leave` lifecycle without WebRTC. ## Show avatars and caret labels `DocxEditorCollaboration` provides presence controls. Import it from `@docx-editor.dev/pro/react` or `@docx-editor.dev/pro/vue`. Mount its parts anywhere in the editor provider tree. `DocxEditorCollaboration.Avatars` renders participant initials. It puts the local participant first. ```tsx ``` Avatar colors match each collaborator's tracked changes and comments. `max` collapses extra avatars into a `+N` chip. Use `DocxEditorCollaboration.Avatar` to render one participant. You can replace each avatar disc with a renderer. It receives `participant`, `color`, `initials`, and the locally resolved optional `avatarUrl`. ```tsx {({ participant, avatarUrl, initials }) => avatarUrl ? ( {participant.name} ) : ( {initials} ) } ``` `participant` contains `actorId`, `name`, optional `color`, optional `role`, and `isLocal`. Avatars show the picture declared for a collaborator, so one declaration covers every surface that draws that person: ```tsx ``` The review card, the caret label, and the avatar stack all resolve it by display name, which is the string `w:author` carries in the saved file. A declared color outranks the one a peer publishes in `identity.color`: the declaration is your own record of who someone is, and a peer must not be able to make their caret disagree with their comment cards. `identity.color` still applies to anyone you have not declared. `DocxEditorCollaboration.CaretLabels` replaces each remote caret label. The engine positions and colors the label. Your renderer mounts in the adapter tree, so editor and review hooks work inside it. #### React ```tsx {({ selection, participant, color, avatarUrl }) => ( )} ``` #### Vue ```vue ``` The renderer receives `selection`, optional `participant`, `color`, and optional `avatarUrl`. In Vue, it is the default scoped slot. Without a renderer, the label shows the collaborator's name. The engine marks the label layer `aria-hidden` and disables pointer events. Screen readers do not announce label content. Label content cannot receive clicks or focus. Put interactive or announced presence controls in your own chrome. For CSS styling, use the `docx-remote-caret-label` class, `--doc-remote-color` custom property, and `data-docx-remote-actor` attribute. ## Recover a diverged replica A status of `error` is terminal. The replica refused an update and kept its copy. The session now refuses edits, and other peers might not have its latest changes. Waiting does not fix the replica. Call `rejoin`: ```tsx const { rejoin } = useHocuspocusCollaboration({ room }); async function rejoinRoom() { const saved = await editorRef.current?.save(); if (saved) { await rejoin(new Uint8Array(saved)); } } ``` Save the editor bytes before you call `rejoin`. It leaves with those bytes, then joins the same room with `{ kind: 'join' }`. A successful join uses the room copy. It can drop unreplicated changes that existed when the session failed. After a failed join, the saved bytes stay mounted locally. ## Edit offline Set `offlineEditing: true` in the `room` or `connect` options. The session then accepts edits while its status is `disconnected`. It merges buffered updates after reconnection. Show the status so users know when edits have not reached the room. The `error` status remains terminal. Every room factory and hook accepts this option. ## Use your own signaling `DEMO_SIGNALING_ENDPOINTS` is a public demo signaling service. Do not use it for production. For production, pass your signaling URLs to `connect` or `room`. Also provide your own Traversal Using Relays around NAT (TURN) servers. Many networks block direct WebRTC connections without TURN. ## Connect to a Hocuspocus server `useHocuspocusCollaboration` owns a room on a [Hocuspocus](https://tiptap.dev/docs/hocuspocus) server. It has the same options, return values, and lifecycle as `useWebrtcCollaboration`. A server-backed room does not need signaling or TURN configuration. Import the hook from `@docx-editor.dev/pro/react/hocuspocus`. Import the Vue composable from `@docx-editor.dev/pro/vue/hocuspocus`. ```tsx import { DocxEditor } from '@docx-editor.dev/react'; import { useHocuspocusCollaboration } from '@docx-editor.dev/pro/react/hocuspocus'; export function ServerBackedEditor({ roomId, token, bytes, actorId, name, }: { roomId: string; token: string; bytes: Uint8Array; actorId: string; name: string; }) { const { document, modules, session, pending, error } = useHocuspocusCollaboration({ room: { url: 'wss://collab.example.test', roomId, token, identity: { actorId, name }, bootstrap: { kind: 'create-or-join', document: bytes }, }, }); if (error) return

{error.detail ?? error.code}

; if (pending || !document) return

Connecting…

; return ; } ``` `token` reaches the server's `onAuthenticate` hook. If tokens expire, pass a callback instead of a string. The provider calls it for each reconnection. A rejected token during the initial join produces `initialization-aborted`. After a successful join, rejection sets the status to `error` and `error.code` to `authentication-failed`. Handle that code by refreshing the credential and calling `rejoin`. A `transport-disconnected` failure recovers on its own. `syncedTimeoutMs` limits the initial sync wait. Its default is 30 seconds. A timeout produces `initialization-timeout`. The `createHocuspocusCollaboration` factory accepts the same options. Import it from `@docx-editor.dev/pro/collaboration/hocuspocus`. It returns the room, `ydoc`, and `provider`. The Hocuspocus v4 server runs on Node, not Bun. Use `@hocuspocus/provider` for the required authentication handshake. ## Read a room from a server `readCollaborationDocument(ydoc)` returns the room's document as `.docx` bytes. Use it for export, autosave to your own storage, search indexing, or rendering. ```ts import { writeFile } from 'node:fs/promises'; import type * as Y from 'yjs'; import { readCollaborationDocument } from '@docx-editor.dev/pro/collaboration'; async function exportRoom(documentName: string, document: Y.Doc) { await writeFile(`${documentName}.docx`, readCollaborationDocument(document)); } ``` Call `exportRoom` from Hocuspocus `onStoreDocument`, which receives the synchronized `Y.Doc`. It joins nothing. There is no identity, no `Awareness`, and no session, so the job never appears in a room's participant list, and it creates no editing gate. It does not write to the `Y.Doc`. The `Y.Doc` must already hold the room's state. Connect your provider and wait for its initial sync first. A document nobody seeded throws `not-initialized` instead of returning a truncated file. The function throws `CollaborationSchemaError` for `not-initialized`, `concurrent-seed`, `blob-digest-mismatch`, materialization failures, and resource-limit failures. Handle the error and keep the last valid export. See the [server-backed Hocuspocus example](https://github.com/eigenpal/docx-editor/tree/main/examples/collaboration-hocuspocus) for persistence and DOCX export. ## The room is the document While a room is live, the room holds the authoritative copy. An exported `.docx` file is a snapshot of the room at one moment, not a branch of it. A file edited outside the room cannot merge back in. Seeding the edited file creates a new room with new identity, and no three-way merge exists between a room and an external copy. This is the same rule the [recovery flow](#recover-a-diverged-replica) applies within a room: when two copies disagree, the room copy wins. Keep one authority per document at a time: - While people collaborate, treat the room as the document. Export snapshots for backup, indexing, or review, and treat them as read-only. - When collaboration ends, export the room and make the file the authority again. - To bring external edits into a live room, apply them as edits inside the room — for example, paste the changed content — rather than re-seeding the file. - To restart collaboration on an externally edited file, create a new room from those bytes and retire the old room. ## Use the replication contracts `@docx-editor.dev/core/collaboration/replication` holds the seam a replication implementation binds to: the document port an adapter writes through, the primitive journal it reads, and the descriptors that journal is made of. ```ts import type { CollaborationDocumentPort, CanonicalPrimitiveJournal, } from '@docx-editor.dev/core/collaboration/replication'; ``` A host that renders presence and reads a status needs none of it, which is why it is a separate subpath. Import `@docx-editor.dev/core/collaboration` for the consumer types: identity, participants, remote selections, status, and failures. ## Integrate a custom Yjs provider If you own a `Y.Doc` and awareness instance, call `createDocumentCollaboration` from `@docx-editor.dev/pro/collaboration`. It replicates the complete canonical package. You must destroy these resources. Four rules make a bring-your-own-provider integration work: 1. Connect the provider before you use `bootstrap: { kind: 'join' }`. The factory reads synchronized shared state. Without a connection, the join fails with `initialization-timeout` after 30 seconds. 2. Pass an `Awareness` instance from `y-protocols/awareness`. It carries presence and remote selections. 3. Send provider connection events to `session.setTransportStatus`. Without this call, the status remains `ready` during an outage. 4. Some transports limit message size. The WebRTC wrapper provides message framing. A WebSocket provider does not need it. This example wires `y-websocket`: ```ts import * as Y from 'yjs'; import { Awareness } from 'y-protocols/awareness'; import { WebsocketProvider } from 'y-websocket'; import { createDocumentCollaboration } from '@docx-editor.dev/pro/collaboration'; const ydoc = new Y.Doc(); const awareness = new Awareness(ydoc); const provider = new WebsocketProvider('wss://example.test', 'room-1', ydoc, { awareness }); declare const currentUser: { id: string; name: string }; const identity = { actorId: currentUser.id, name: currentUser.name }; await new Promise((resolve) => provider.once('sync', resolve)); const room = await createDocumentCollaboration({ ydoc, awareness, documentId: 'room-1', identity, bootstrap: { kind: 'join' }, }); provider.on('status', ({ status }: { status: string }) => { room.session.setTransportStatus( status === 'connected' ? 'ready' : 'disconnected', status === 'connected' ? undefined : 'transport-disconnected', status === 'connected' ? undefined : 'websocket disconnected' ); }); ``` The second argument is a `CollaborationFailureCode`, not free text. Use `transport-disconnected` for a socket that retries itself and `authentication-failed` for a credential the server rejected. Those need opposite responses from the host, so they must not share a code. Put your provider's own wording in the third argument. The factory rejects with a typed `CollaborationSchemaError`. Handle these codes: - `initialization-timeout`: No synchronized room appeared. - `document-id-mismatch`: The room has a different `documentId`. - `protocol-version-mismatch`: The room uses a different protocol version. - `schema-version-mismatch`: The room uses a different schema version. Use Yjs 13 on the server. The `y-websocket` server (`bin/server.cjs`) and Hocuspocus support it. `@y/websocket-server` targets the Yjs 14 release candidate. It synchronizes initial state and presence, but not live Yjs 13 updates. This incompatibility makes the document appear frozen. ### Recover from an error An `error` session cannot repair itself. Recover it as follows: 1. Save the editor bytes with `await editor.save()`. 2. Call `room.destroy()`, then destroy the provider. 3. Create a new `Y.Doc`, `Awareness`, and provider. 4. Connect the provider. 5. Call `createDocumentCollaboration` with `bootstrap: { kind: 'join' }`. Keep the editor mounted with the saved bytes until the room is ready. If the join fails, the saved bytes preserve the local work. The same entry exports the experimental `createTextCollaboration`. It only replicates paragraph text and rejects structural edits. Use `createDocumentCollaboration`. ## Create a headless replica `DocxEditor.createCollaborative` from `@docx-editor.dev/editor-api` opens a Document Object Model (DOM)-free replica. ```ts import { DocxEditor } from '@docx-editor.dev/editor-api'; import { createDocumentCollaboration } from '@docx-editor.dev/pro/collaboration'; const room = await createDocumentCollaboration({ ydoc, awareness, documentId: 'room-1', identity: { actorId: 'agent', name: 'Agent', role: 'agent' }, bootstrap: { kind: 'join' }, }); const runtime = await DocxEditor.createCollaborative(room.document, room.session, { author: 'Agent', }); ``` When the job finishes, call `runtime.dispose()` and then `room.destroy()` to release the runtime and stop the replica. ## Let a server agent propose redlines A background worker can join the same Hocuspocus room as the browser peers with `role: 'agent'`, then attach `DocxEditor.createCollaborative` as above. The worker owns its connection, so closing the initiating browser does not stop its job. Set the tracking mode before editing to create Word tracked changes: ```ts await runtime.run(async (context) => { const matches = context.document.body.search('within 7 days', { matchCase: true }); matches.load('items'); await context.sync(); if (matches.items.length !== 1) throw new Error('Choose a unique target'); const range = matches.items[0]!; range.load('text'); await context.sync(); // Decide from the loaded snapshot; sync refuses if the replica changed meanwhile. context.document.changeTrackingMode = 'TrackMineOnly'; range.insertText('within 30 days', 'Replace'); await context.sync(); }); ``` `range.insertText(text, 'Before' | 'After')`, `range.delete()`, and `range.clear()` use the same transaction path. Configure an `author` on the runtime. `TrackMineOnly` persists across runs and applies only to that server host. Peers retain their own editing mode. `Off` makes ordinary edits. `TrackAll` and browser-host mode control refuse with `NotSupported`. The local mode is not saved as a document-wide policy. Tracked edits currently support inline text within one paragraph, including table cells. Targets touching pending revisions, structural changes, and formatting changes under tracking refuse atomically. Commit one logical suggestion per sync. Send progress through a separate job/event channel, while the collaboration session publishes committed redlines to peers. On a stale snapshot, reread and reconsider the edit. A local revision check cannot see remote changes that have not reached the worker yet. The [server agent review example](https://github.com/eigenpal/docx-editor/tree/main/examples/server-agent-review) includes a React review room, Hocuspocus persistence, a Node worker, scripted and AI modes, snapshot tokens, deduplication, cancellation, and browser-independent jobs. It also shows how to wait for outbound transport acknowledgement and clean up both the runtime and the room. Browser peers need the review module to display and accept or reject suggestions. ## Compose custom controls Add custom controls as children of the collaboration root shown in [Mount the editor](#mount-the-editor). The root supplies `key`, `document`, `modules`, and the default author. Status and presence hooks inside it find the session without a `session` argument. In Vue, wrap the composable return with `reactive` before you pass it to `DocxEditorCollaborationRoot`. Let the room hook manage cleanup. An effect cleanup can destroy the room before React StrictMode remounts the component. ## Collaboration behavior and limits The WebRTC helper connects peers directly. A room exists while at least one peer remains connected. Attached replicas synchronize comments, tracked-change decisions, tables of contents, and custom nodes. Named review actions include these operations: - Add, reply to, resolve, and delete comments. - Resolve tracked changes. - Write package-scoped content, such as tables of contents and custom nodes. The replica rejects two write paths. It rejects an edited ProseMirror document. It also rejects review writes without a named intent. The replica rejects tree edits when the session is destroyed, not ready, or not attached. A transport interruption pauses editing until reconnection. To continue editing, enable [`offlineEditing`](#edit-offline). The session undo manager treats one typing run as one undo step. One simultaneous run-formatting split converges without duplicate text. A later split after one concurrent run-formatting round can duplicate text. All replicas still converge on the same document. ### Numeric limits Most exceeded limits produce a typed failure code. | Limit | Value | Failure code | Remedy | | ----------------------------------- | -------------- | ------------------------------------------- | ------------------------------------------------------ | | Seed document | 20 MB | `baseline-too-large` | Reduce the document. | | One embedded file | 32 MiB | `blob-too-large` | Compress the media. | | All embedded files | 64 MiB | `blob-store-full` | Remove media and create a new room. | | `actorId`, `name`, and `documentId` | 256 characters | `invalid-identity` or `invalid-document-id` | Shorten the value. | | Presence participants | 256 | None | Keep the room below 256 participants. | | Initial synchronization | 30 seconds | `initialization-timeout` | Connect the provider and confirm that the room exists. | Presence reads return only the first 256 participants. For Hocuspocus, increase `syncedTimeoutMs` when the initial synchronization needs more time. ### Watch a room's size A room only grows. Deletion writes a tombstone, and deleted media bytes stay in the shared state, so a long-lived room under heavy editing moves toward the node and media limits. Crossing a limit is a terminal error for the room. Watch the growth and archive the room before that happens. `session.resourceUsage()` returns the replicated counts next to the limits: ```ts const usage = session.resourceUsage(); if (usage.nodes > usage.maxNodes * 0.8) { // Export the room and create a new one from the saved bytes. } ``` On a server, call `readCollaborationResourceUsage(ydoc)` from `@docx-editor.dev/pro/collaboration`. It reads a synchronized `Y.Doc` the same way `readCollaborationDocument` does: it joins nothing and writes nothing. Both probes walk the node map once per call. Read them on a schedule, not on every edit. For module registration and licensing, see the [Pro package documentation](/docs/2.x/pro). --- # Comments Source: https://www.docx-editor.dev/docs/2.x/pro/comments Comments attach a discussion to a text range. The editor reads existing OOXML comments during load. It shows them beside the page and writes them during save. You can continue threads created in Microsoft Word. Word can also continue threads created in this editor. Comments share one sidebar, hook, and card layout with [tracked changes](/docs/2.x/pro/tracked-changes). Comments require the review module from [`@docx-editor.dev/pro`](/docs/2.x/pro). ## Prerequisites | Task | Requirement | | ------------------------------------- | -------------------------------------------------------------------------------- | | Read comments | A loaded document | | Show the review rail | `reviewModule()` and `DocxEditorReview` | | Create or reply in a browser | A non-empty `author`, `reviewModule()`, an editable mode, and an attached editor | | Resolve, reopen, or delete in browser | `reviewModule()`, an editable mode, and an attached editor | | Create or reply on a server | A non-empty `author` and a server runtime | OOXML requires `w:author` for each comment and reply. The engine refuses a write without an author. For module registration and licensing, see the [Pro package documentation](/docs/2.x/pro). ## Write a comment The review hook's `comment(text, author?)` comments on the current selection. It returns whether the editor applied the write. Keep the draft text when the method returns `false`. This example keeps the draft after a refused write: #### React ```tsx import { useState } from 'react'; import { useReview } from '@docx-editor.dev/pro/react'; function CommentBox() { const { comment, selectionAnchorY } = useReview(); const [text, setText] = useState(''); // null when nothing is selected: there is nowhere to anchor a comment. if (selectionAnchorY === null) return null; return (
{ e.preventDefault(); if (comment(text)) setText(''); }} >