@extend-ai/react-docx
v0.14.4
Published
JavaScript and React viewers for DOCX documents
Readme
@extend-ai/react-docx
JavaScript and React viewing and editing components for rendering .docx files in a browser.
@extend-ai/react-docx gives you:
- A shared viewer for read-only previews and editable documents
- A controller API for import/export and custom DOCX editing UIs
- Pagination, page layout, theme, tracked-change, form-field, and thumbnail hooks
- Configurable page surface and inter-page background colors
- A dark read-only night-reader mode that inverts document content while preserving image hues
- Lower-level OOXML, model, layout, and serialization exports for custom pipelines
Install
pnpm add @extend-ai/react-docxFor the React entry, install react and react-dom as well. The vanilla entry bundles Preact and does not require React.
Benchmarks and chart accuracy
See Rust Office for parser benchmarks, accuracy results, and chart comparisons with Microsoft Word.
WebAssembly Asset
DOCX parsing and serialization run in a Rust/WebAssembly module that ships inside this package as dist/docx_wasm_bg.wasm (~2.5 MB raw, ~1 MB over the wire with gzip). It is not part of the JavaScript bundle:
- It loads lazily, on the first call that parses or serializes a document.
- The loader references it as
new URL("./docx_wasm_bg.wasm", import.meta.url), which most Vite, webpack 5, Rollup, and Next.js builds emit as a hashed static asset. - In Node (SSR, tests, scripts) the binary is read from
node_moduleson disk.
If your deployed app returns 404 for the .wasm file, or if you need to host the binary somewhere else (e.g. a CDN), override the source before the first parse:
import { setWasmSource } from "@extend-ai/react-docx";
setWasmSource("https://cdn.example.com/docx_wasm_bg.wasm");
// or pass a URL, Response, ArrayBuffer/TypedArray, or compiled WebAssembly.ModuleThe binary is also exposed as a package subpath, so with Vite you can do:
import wasmUrl from "@extend-ai/react-docx/docx_wasm_bg.wasm?url";
import { setWasmSource } from "@extend-ai/react-docx";
setWasmSource(wasmUrl);Configured string, URL, Request URL, bytes, and WebAssembly.Module sources are forwarded into the DOCX import worker. Response sources are supported on the main thread; imports automatically avoid the worker for those.
You can also call initWasm() (optionally with a source) ahead of time to warm the module before the first document is opened.
Vanilla applications can import setWasmSource and initWasm from @extend-ai/react-docx/vanilla. The React and vanilla entries share the same WASM configuration.
In Vite development, exclude the vanilla entry from dependency optimization so Vite can resolve the import worker asset. Production builds do not need this setting:
import { defineConfig } from "vite";
export default defineConfig({
optimizeDeps: { exclude: ["@extend-ai/react-docx/vanilla"] },
});Main Entry Points
@extend-ai/react-docx/vanilla exports createDocxViewer, the DocxViewer instance class, and utilities for JavaScript applications.
Use useDocxEditor + DocxEditorViewer for both read-only and editable
experiences. The hook manages document state, import/export, and editing
commands; the component renders the document. Set mode="read-only" for
view-only use or mode="edit" for editing.
It also re-exports lower-level document/model/layout/serializer APIs, so you can work below the UI layer when needed.
Quick Start
Vanilla JavaScript
Mount a viewer in an existing element using a browser bundler such as Vite. The viewer uses a bundled Preact renderer and creates a div in the regular DOM.
Run pnpm dev from the repository root and open /vanilla.html on the local playground URL to try the standalone demo.
Open a document from a URL
<div id="viewer" style="height: 600px; overflow: auto"></div>
<script type="module" src="/main.js"></script>import { createDocxViewer } from "@extend-ai/react-docx/vanilla";
const viewer = createDocxViewer(document.getElementById("viewer"), {
src: "/document.docx",
});The URL must be accessible to the browser. A document on another origin needs appropriate CORS headers. The viewer defaults to mode: "read-only"; use mode: "edit" to enable editing.
You can also construct an instance directly:
import { DocxViewer } from "@extend-ai/react-docx/vanilla";
const viewer = new DocxViewer(document.getElementById("viewer"), {
src: "/document.docx",
mode: "edit",
});Open a local document
This example starts with an empty viewer and loads a document selected by the user:
<input id="file" type="file" accept=".docx,.doc" />
<div id="viewer" style="height: 600px; overflow: auto"></div>
<script type="module" src="/main.js"></script>import { createDocxViewer } from "@extend-ai/react-docx/vanilla";
const viewer = createDocxViewer(document.getElementById("viewer"), {
emptyState: "Choose a document to preview.",
});
document.getElementById("file").addEventListener("change", (event) => {
const file = event.currentTarget.files?.[0];
if (file) viewer.setOptions({ file });
});file accepts a File, Blob, or ArrayBuffer. A File uses its own name by default; a Blob or ArrayBuffer uses document.docx unless fileName is supplied. An explicit fileName must end in .docx or .doc.
Read state and control the viewer
Subscribe to loading, errors, pagination, editing, and other controller updates:
const unsubscribe = viewer.subscribe((controller) => {
if (controller.isLoading) {
console.log("Loading document");
} else if (controller.error) {
console.error(controller.error);
} else if (controller.hasDocument) {
console.log(controller.fileName, controller.currentPage, controller.totalPages);
}
});Read the current controller when invoking commands. For example, after the document has loaded:
viewer.setOptions({ mode: "edit" });
const controller = viewer.getController();
if (controller?.hasDocument && !controller.isLoading) {
controller.setZoom("fit-width");
controller.exportDocx();
}exportDocx() downloads the current document in the browser, and getDocxBytes() resolves to the same bytes as an ArrayBuffer for saving elsewhere, such as uploading to your own storage. The controller also exposes commands such as undo(), redo(), and setDocumentTheme().
Change options or load another document with setOptions:
viewer.setOptions({ mode: "read-only", pageGapBackgroundColor: "#f3f4f6" });
viewer.setOptions({ src: "/another-document.docx", fileName: undefined });Supplying file clears the previous src, and supplying src clears the previous file. Passing both in the same update throws. An explicit fileName remains in effect until changed or cleared with fileName: undefined. Changing the source, its name, parsed document, or starter model creates a fresh controller; other viewer options update the existing viewer.
The initialDocumentTheme option applies when a controller is created. To change the current document's theme and retain it when loading another source:
viewer.setOptions({ initialDocumentTheme: "dark" });
viewer.getController()?.setDocumentTheme("dark");When the viewer is no longer needed, unsubscribe and release its resources:
unsubscribe();
viewer.destroy();createDocxViewer returns an instance with these methods:
getController()returns the latestDocxVanillaViewerController, ornullbefore it is available and after destruction. The controller includes the editor's commands and state, plusisLoading,error, andhasDocument.subscribe(listener)reports controller updates and immediately calls the listener when a controller is available. It returns an unsubscribe function; updates can be coalesced.setOptions(options)merges new options into the current configuration.reload()discards the current viewer state and reloads the configured source.destroy()unmounts the viewer, removes its element, and releases listeners. It is safe to call more than once.
DocxVanillaViewerOptions supports page virtualization, zoom, themes, page colors, tracked changes, comments, and context menus. emptyState, loadingState, errorState, and custom card and context-menu renderers accept text or DOM nodes. The vanilla entry also exports parseDocxForViewer, setWasmSource, and initWasm.
Read-only viewer
Starting in v0.10.0, the intended pattern for read-only viewing is
useDocxEditor with DocxEditorViewer mode="read-only". Use the same component
with mode="edit" when editing is needed.
import { DocxEditorViewer, useDocxEditor } from "@extend-ai/react-docx";
export function ReadOnlyDocxExample() {
const editor = useDocxEditor();
return (
<div style={{ display: "grid", gap: 12 }}>
<input
type="file"
accept=".docx"
onChange={(event) => {
const file = event.target.files?.[0];
if (file) {
void editor.importDocxFile(file);
}
}}
/>
<DocxEditorViewer editor={editor} mode="read-only" />
</div>
);
}For an already parsed document, initialize the controller with
useDocxEditor({ document }). For a prebuilt document model, use
useDocxEditor({ starterModel: model }). To start from a raw buffer, call
parseDocxForViewer(buffer) and pass the resulting document to the hook.
These options set the initial document; use editor.importDocxFile(file) to
load a different document into an existing controller.
Full editor/viewer
Use useDocxEditor when you want import/export controls, document theme state, selection-aware editing commands, pagination state, and a customizable document canvas.
import * as React from "react";
import {
DocxEditorViewer,
useDocxDocumentTheme,
useDocxEditor,
useDocxPagination,
} from "@extend-ai/react-docx";
export function EditorExample() {
const editor = useDocxEditor({
initialDocumentTheme: "light",
initialStatus: "Ready",
});
const { documentTheme, toggleDocumentTheme } = useDocxDocumentTheme(editor);
const { pagination } = useDocxPagination(editor);
return (
<div style={{ display: "grid", gap: 12 }}>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<input
type="file"
accept=".docx"
onChange={(event) => {
const file = event.target.files?.[0];
if (file) {
void editor.importDocxFile(file);
}
}}
/>
<button type="button" onClick={() => editor.exportDocx()}>
Export DOCX
</button>
<button type="button" onClick={() => toggleDocumentTheme()}>
Theme: {documentTheme}
</button>
<span>
Page {pagination.currentPage} / {pagination.totalPages}
</span>
<span>{editor.status}</span>
</div>
<DocxEditorViewer
editor={editor}
mode="edit"
pageBackgroundColor="#ffffff"
pageGapBackgroundColor="transparent"
/>
</div>
);
}Thumbnail-only rendering
Use the imperative renderer when a cover or page preview is needed before the
full viewer mounts. The parsed document can be passed to useDocxEditor later,
so the source is read and parsed only once.
import {
createDocxThumbnailRenderer,
parseDocxForViewer,
useDocxEditor,
} from "@extend-ai/react-docx";
const document = await parseDocxForViewer(file, {
loadEmbeddedFonts: false,
});
const renderer = createDocxThumbnailRenderer(document, {
scheduling: "immediate",
pixelRatio: 1,
pageIndexes: [0],
});
const cover = await renderer.renderPage(0, {
maxWidth: 240,
output: "blob",
});
function FullEditor() {
const editor = useDocxEditor({ document });
return <DocxEditorViewer editor={editor} />;
}Responsive zoom
DocxEditorViewer accepts numeric percentages and responsive zoom modes.
Responsive modes remain selected while the viewer recalculates their resolved
percentage as its viewport or page layout changes.
<DocxEditorViewer
editor={editor}
mode="read-only"
defaultZoom="fit-width"
/>Use zoom with onZoomChange for a controlled viewer, or defaultZoom for an
uncontrolled viewer. The available modes are "automatic", "fit-page", and
"fit-width". Custom editor toolbars can use editor.setZoom(level),
editor.getZoom(), and editor.getResolvedZoom(). When zooming in or out from
a responsive mode, calculate the next numeric percentage from
editor.getResolvedZoom() and pass it to editor.setZoom().
renderPage supports canvas, blob, and imageBitmap output. Pass canvas
to draw into a caller-owned canvas without an encoding round trip. Its timings
separate pagination, page mounting, rasterization, and encoding; the parsed
document timings separately expose file reading, parsing, model construction,
and embedded-font loading. Use loadEmbeddedFonts: "defer" to load fonts later
through document.loadEmbeddedFonts(). The renderer also accepts an existing
DocModel directly when the OOXML package is not needed.
Examples
See @extend-ai/react-docx in action in these live Extend UI examples. Click a screenshot to open the demo and integration docs.
DOCX Viewer
Preview paginated documents with zoom and page navigation.
DOCX Editor
Try the experimental editor with document formatting, tracked changes, and export.
Caller fonts
The viewer prepares shared fallbacks for Calibri, Arial, Times New Roman, Courier New, Aptos and Aptos Narrow before pagination. It loads only requested families, preserving embedded and installed originals. Chart SVG images embed the supported font families they use because image documents cannot inherit the page's fonts. Font payloads are lazy chunks; deploy them alongside the viewer. Redistribution notices are in dist/Office-Font-Licenses.txt.
Fonts embedded in a DOCX load automatically by default. Caller-provided fonts
must be available in document.fonts before layout uses them. When adding or
removing fonts after parsing or mounting, call refreshDocxFontMetrics() after
the change to refresh cached text measurements and mounted viewers.
import { refreshDocxFontMetrics } from "@extend-ai/react-docx";
async function attachViewerFont(face: FontFace) {
await face.load();
document.fonts.add(face);
refreshDocxFontMetrics();
return () => {
document.fonts.delete(face);
refreshDocxFontMetrics();
};
}Provide matching font data and descriptors for each required weight and style.
If a reference renderer uses different vertical metrics for the same font,
the caller can supply verified ascentOverride, descentOverride, and
lineGapOverride descriptors on the FontFace. These affect browser layout
as well as viewer measurements. Bind such a profile to the actual font bytes
and reference environment; a family name or document charset alone does not
establish the correct metrics.
The caller owns loading and cleanup. Load fonts before creating a standalone
createDocxThumbnailRenderer; after changing fonts, dispose and recreate that
renderer to refresh its captured pagination.
Thumbnail Hook
The library can expose page thumbnails so you can build your own page strip, mini-map, or navigation UI. Thumbnail painting can render from the live page surface when it is mounted, or from an offscreen one-page surface when viewer virtualization has unmounted that page.
import * as React from "react";
import {
DocxEditorViewer,
useDocxEditor,
useDocxPageThumbnails,
} from "@extend-ai/react-docx";
export function ThumbnailExample() {
const editor = useDocxEditor();
const { thumbnails } = useDocxPageThumbnails(editor, {
maxWidthPx: 160,
maxHeightPx: 220,
pixelRatio: 2,
minRasterIntervalMs: 40,
pageIndexes: [0, 1, 2, 3, 4, 5],
renderWindow: {
visiblePageIndexes: [0, 1, 2],
prefetchPageIndexes: [3, 4, 5],
},
});
return (
<div style={{ display: "grid", gridTemplateColumns: "180px 1fr", gap: 16 }}>
<div style={{ display: "grid", gap: 12, alignContent: "start" }}>
{thumbnails.map((thumbnail) => (
<canvas
key={thumbnail.pageIndex}
ref={thumbnail.canvasRef}
width={thumbnail.pixelWidthPx}
height={thumbnail.pixelHeightPx}
style={{
width: thumbnail.widthPx,
height: thumbnail.heightPx,
border: "1px solid #ddd",
}}
/>
))}
</div>
<DocxEditorViewer editor={editor} />
</div>
);
}Notes:
- Thumbnail canvases can stay attached in a virtualized sidebar; only canvases you mount request paint work.
- Use
renderWindow.visiblePageIndexesfor thumbnails currently visible in your sidebar, andrenderWindow.prefetchPageIndexesto warm nearby pages after visible work. - Use
pageIndexesas a hard render set when unlisted pages must remain metadata-only. It is distinct from the priority hints inrenderWindow. - Call
thumbnail.renderToCanvas(canvas, { scheduling: "immediate" })for a visible cover that must bypass idle scheduling. minRasterIntervalMscontrols repeat renders for the same canvas. Lower values are useful when the consumer already limits thumbnail work to a small visible window.- Thumbnail sizing is bounded by
maxWidthPxandmaxHeightPx, so downstream UIs can bias toward portrait thumbnail rails. - Thumbnails use a direct layout/model canvas renderer first; if a page has no usable snapshot, the hook falls back to an isolated offscreen page surface.
Useful Hooks
@extend-ai/react-docx exports several hooks for wiring custom UI around the viewer:
useDocxDocumentTheme(editor)for light/dark document modeuseDocxPagination(editor)for current page and total page countuseDocxPageLayout(editor)for page size, margins, columns, and viewport defaultsuseDocxPageThumbnails(editor, options)for rendering page previews into your own canvasesuseDocxParagraphStyles(editor)for available paragraph styles and style selectionuseDocxLineSpacing(editor)for selected line spacing stateuseDocxBorders(editor)for paragraph/table border presetsuseDocxFormFields(editor)for DOCX form-field state and updatesuseDocxTrackChanges(editor)for tracked-change state plus safeacceptChange/rejectChangeactionsuseDocxComments(editor)for comment state, selected-range comment creation, and resolve/reopen actionsuseDocxImageWrapMenu(editor)for image wrapping controls
Annotation mutations intentionally fail closed. Accept/reject currently
supports a single top-level, text-only w:ins or w:del; comment creation
supports one non-empty plain-text range in a top-level paragraph. Moves,
formatting revisions, fields, hyperlinks, tables, nested revisions, drawings,
and cross-paragraph ranges return an unsupported, stale, or unsafe-xml
result without changing the document. Successful actions participate in the
editor's normal undo/redo history. Pass the current DocxTrackedChange or
DocxComment object back to its action; handles are document- and
source-scoped, so a handle retained across an import or conflicting edit
returns stale instead of targeting a reused OOXML id.
Lower-level APIs
The package also re-exports the lower-level internals used by the viewer:
- OOXML parsing from
@extend-ai/react-docx-ooxml-core - Document model helpers from
@extend-ai/react-docx-doc-model - Editing operations from
@extend-ai/react-docx-editor-ops - Layout primitives from
@extend-ai/react-docx-layout-engine - Layout/core snapshot helpers from
@extend-ai/react-docx-layout-core - DOCX serialization from
@extend-ai/react-docx-serializer
This means you can build your own pipeline, for example:
import { buildDocModel, parseDocx, serializeDocx } from "@extend-ai/react-docx";
const pkg = await parseDocx(arrayBuffer);
const model = buildDocModel(pkg);
const output = serializeDocx(model, pkg);Viewer Notes
DocxEditorViewersupportsmode="edit"andmode="read-only".pageBackgroundColorcontrols the page surface color.pageGapBackgroundColorcontrols the area between pages and defaults to transparent.- In read-only dark document mode, the viewer uses an inversion-based night reader path so document content inverts cleanly while images keep their hues.
License
See the repository license for usage terms.


