Build a DOCX viewer
The JavaScript and TypeScript DOCX viewer guide includes a live demo and source code for opening local files, showing progress, cancelling a load, and selecting text.
Install
Ready-to-use Word editor
Install the complete editor shown on the homepage, including its ribbon, document editing controls, undo/redo and Word/PDF downloads.
npm install @onodocs/editorEditor on npm · Embed the Word editor
SDK and Canvas install automatically as dependencies. Use the package directly in your browser application; cloning or building the source is only needed when modifying the Editor source itself. Evaluation needs no license key. Editor uses MIT; its SDK and Canvas dependencies have separate evaluation and commercial terms.
SDK and viewer
The SDK processes documents in the browser or Node.js. Canvas provides the browser viewer, page painting, selection and HTML attachments.
npm install @onodocs/sdk @onodocs/canvasSDK on npm · Canvas on npm · Release archives
Install matching versions. A backend doing analysis or image/PDF export needs only the SDK. A frontend receiving prepared pages needs only Canvas. The public repository contains runnable JavaScript and TypeScript samples. Evaluation and commercial terms apply.
| Import | Purpose |
|---|---|
@onodocs/editor | Embed the complete Word editor with its ribbon and editing controls. |
@onodocs/sdk | Inspect and query documents in Node.js or the browser. |
@onodocs/sdk/browser | Process documents for display in a browser. |
@onodocs/sdk/server | Export images and PDFs with headless Chromium. |
@onodocs/canvas | Paint pages, mount views, select text and attach HTML controls. |
Examples below focus on the operation being demonstrated. See Errors and cleanup for shared lifecycle handling and the API reference for signatures, options, and return values.
Render in the browser
Add an empty document host and status element to your page. The complete viewer sample includes the application shell, styles, a sample document and setup instructions.
<output id="status" role="status">Opening document…</output>
<div id="pages"></div>In your browser module, fetch your document URL and mount the rendered pages:
import { openDocument } from "@onodocs/sdk/browser";
import { createDocument } from "@onodocs/canvas";
const container = document.querySelector("#pages");
const statusElement = document.querySelector("#status");
const controller = new AbortController();
let doc, canvasDocument, view;
window.addEventListener("pagehide", event => {
if (!event.persisted) {
controller.abort();
doc?.dispose();
}
});
try {
const response = await fetch("/sample.docx", { signal: controller.signal });
if (!response.ok) throw new Error("Document is unavailable.");
const bytes = await response.arrayBuffer();
doc = await openDocument(bytes, { signal: controller.signal });
canvasDocument = createDocument(doc);
view = canvasDocument.mount(container, { zoom: "fit-width" });
await view.whenRendered();
statusElement.textContent = `${doc.pages.length} pages. Select text to copy it.`;
} catch (error) {
doc?.dispose();
if (!controller.signal.aborted) statusElement.textContent = error.message;
}Pass a DOCX, DOCM, DOTX or DOTM File from a file input directly to openDocument. Macros are not executed. Give the container a usable width. fit-width follows resizing. The content-query, attachment, update and geometry snippets below use this doc, canvasDocument and view. Add those snippets inside the try block after await view.whenRendered().
Load large documents progressively
In the browser module above, replace the three lines that open the document, create the Canvas document and mount the view with this block. Keep the fetching, error handling and cleanup around it.
doc = await openDocument(bytes, {
signal: controller.signal,
onProgress(progress) {
canvasDocument ??= createDocument(progress.document, {
container, viewOptions: { zoom: "fit-width" }
});
statusElement.textContent = `Loading… ${progress.document.pages.length} pages available`;
}
});
view = canvasDocument.view;Early pages can change as pagination settles. Opening resolves with the complete document. The complete viewer sample adds a file picker, a Cancel button and document replacement. Scrolling paints nearby pages and releases distant canvases, while document content and layout still consume memory proportional to document size.
Add editing and business workflows
Use the document workflow guide to embed the editor, generate proposals, collect controlled answers, review agreements and connect extracted passages to your AI service. Each workflow has a runnable browser demo and complete source. The complete Editor source is MIT-licensed and can be built and customized. SDK and Canvas remain separately commercially licensed.
Embed a license
One application license covers SDK, Canvas and self-hosted capabilities for the named product, including its frontend, backend, background jobs and collaboration. See application scope and redistribution terms.
Pass the license key issued by OnoDocs when opening a document. It can be included in your application bundle. Verification happens offline and requires HTTPS or localhost in a browser.
const doc = await openDocument(bytes, { licenseKey: "YOUR_SIGNED_LICENSE" });Without a key, the SDK runs in non-production evaluation mode with watermarked output. A trial key removes the watermark for 30 days. Commercial keys keep working indefinitely for covered SDK releases. Request a trial or choose a commercial license on the pricing page.
Extract text and tables
Follow the JavaScript and TypeScript DOCX extraction guide to read paragraphs, select tables by headers and export JSON in the browser or Node.js. The guide includes a live demo and complete sample source.
Inspect document content
const paragraphs = doc.query.paragraphs().all();
const dates = doc.query.contentControls().where({ tag: "delivery-date" }).all();
const matches = doc.query.findText("Client approval:").all();Choose an element type, filter it, then select the results. Use one() when exactly one match is required, optional() for zero or one, and all() for every match.
Read a table
const table = doc.query.tables()
.where({ headers: ["Deliverable", "Owner", "Due"] }).one();
const rows = table.textRows.slice(1);textRows includes the header row, so slice(1) selects the data rows. Query again after updates to read current values.
Scope a query
const cells = doc.query.within(table).cells().all();
const headers = doc.query.stories().where({ story: "header" }).paragraphs().all();Queries work independently of rendering. See query selectors and filters, document elements, and text ranges.
Attach HTML controls
Define ordinary controls in HTML, then anchor them to document content. This example uses the first delivery-date control in the supplied project brief.
<input id="delivery-date" aria-label="Delivery date">
<button id="submit">Submit</button>const field = doc.query.contentControls().where({ tag: "delivery-date" }).first();
const input = document.querySelector("#delivery-date");
input.value = field.text;
view.attach(input, { anchor: doc.geometry.fragments(field)[0] });
view.attach(document.querySelector("#submit"), {
anchor: doc.pages[0],
placement: "inside-bottom-right", inset: 480,
size: { width: 2400, height: 600 }
});Placement, size, and spacing use document geometry. Your application handles the controls’ values and events. Content spanning multiple fragments needs an explicit fragment anchor; see attachment options.
The Custom form sample shows editable delivery dates, approval, and submission.
Update text and fill templates
Create a tagged content control in Word, then fill it with application data. For this example, open the booking template instead of sample.docx.
const customer = doc.query.contentControls().where({ tag: "customer" }).one();
await doc.update({ target: customer, text: "Willow Design" });Updates refresh the rendered pages. Read queries and geometry again afterward, and reattach any HTML controls. The original file stays unchanged. Save the current document with await doc.save(). For generation with repeated sections, images and conditional content, see the template workflow. See update targets and constraints.
Pages and coordinates
Zoom and render a page
view.setZoom(1.25);
view.setZoom("fit-width");
const canvas = document.createElement("canvas");
await canvasDocument.pages[0].render(canvas, { dpi: 144 });Page indices are zero based. Page coordinates use twips: 1440 units per inch.
Find the content under a click
view.element.addEventListener("click", event => {
const hit = view.hitTest({ x: event.clientX, y: event.clientY });
console.log(hit ? doc.query.get(hit.elementId) : undefined);
});Search and reveal a match
const match = doc.query.findText(/approval/i).first();
const fragment = match && doc.geometry.fragments(match)[0];
if (fragment) view.scrollTo(fragment, { block: "center" });See coordinate conversion and geometry and hit results for return values and offset units.
Errors and cleanup
Handle rejected SDK promises in your application and check response.ok when fetching documents. Pass an AbortSignal to cancel work when the source changes or the component disappears. Release browser documents when no longer needed. This also removes their views and attachments. Call dispose() on analysis documents when finished to release their retained source package.
The shared mount helper handles fetching, progress, errors, cancellation and cleanup. Save it as mount.ts beside your component. Pass callbacks to display errors and loading status in your application.
import { mountDocument } from "./mount";
const host = document.querySelector<HTMLElement>("#pages")!;
const stop = mountDocument(host, "/sample.docx", console.error, console.log);Call stop() when replacing the document or removing its host. The framework excerpts below connect it to their lifecycle hooks. For server rendering, dispose documents in finally and dispose the renderer when the service ends.
Use your framework
Connect document loading to your framework's mount and cleanup hooks. These excerpts use the shared mount helper, saved beside the component as mount.ts. It accepts a local File or document URL and returns a function that cancels loading and releases the view. Leave the host element empty and give its scrolling parent a usable height.
Each linked GitHub project includes the complete application, dependencies, a sample document and setup instructions.
React
Return the helper's cleanup function from the effect. React calls it before loading a new source and when the component unmounts.
"use client";
import { useEffect, useRef } from "react";
import { mountDocument } from "./mount";
export function DocumentViewer({ source }: { source: string | File }) {
const host = useRef<HTMLDivElement>(null);
useEffect(() => mountDocument(host.current!, source), [source]);
return <div ref={host} />;
}
Complete React sample · React integration guide.
Next.js
Use the same pattern in a Client Component, beginning with "use client" as above. The helper imports the browser SDK after mounting, so the initial page can still render on the server.
Vue
Watch the source after mounting. The watcher cleanup releases the previous document on replacement or unmount.
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from "vue";
import { mountDocument } from "./mount";
const props = defineProps<{ source: string | File }>();
const host = ref<HTMLElement>();
let unwatch = () => {};
onMounted(() => {
unwatch = watch(() => props.source, (source, _, onCleanup) => {
onCleanup(mountDocument(host.value!, source));
}, { immediate: true });
});
onUnmounted(() => unwatch());
</script>
<template><div ref="host" /></template>
Nuxt
The Vue component above also works in Nuxt. Keep document loading inside onMounted; the initial component markup can render on the server.
Svelte
Return cleanup from the effect. It runs when the source changes or the component is removed.
<script lang="ts">
import { mountDocument } from "./mount";
let { source }: { source: string | File } = $props();
let host: HTMLDivElement;
$effect(() => mountDocument(host, source));
</script>
<div bind:this={host}></div>
SvelteKit
Use the Svelte component above in a route. The effect runs in the browser, while SvelteKit can render the page shell on the server.
Angular
Open the document after the host exists and register cleanup with DestroyRef. Replace /sample.docx with your application's document URL.
import { Component, DestroyRef, ElementRef, afterNextRender, inject, viewChild } from "@angular/core";
import { mountDocument } from "./mount";
@Component({ selector: "document-viewer", template: "<div #host></div>" })
export class DocumentViewer {
host = viewChild.required<ElementRef<HTMLElement>>("host");
constructor() {
const destroy = inject(DestroyRef);
afterNextRender(() => {
destroy.onDestroy(mountDocument(this.host().nativeElement, "/sample.docx"));
});
}
}
Complete Angular sample with file replacement and loading controls.
ASP.NET Core Razor Pages
Place an empty host in the Razor page and load your bundled browser module:
<div id="pages"></div>
<script type="module" src="~/js/document.js"></script>
In that module, open the document and release it when the page closes:
import { mountDocument } from "./mount";
const host = document.querySelector<HTMLElement>("#pages")!;
const stop = mountDocument(host, "/sample.docx");
window.addEventListener("pagehide", event => {
if (!event.persisted) stop();
});
Word document processing runs in the browser. The complete Razor Pages sample includes JavaScript bundling, static-file configuration and local file controls.
Blazor
The viewer component calls its browser module after rendering. This excerpt passes the component's host element to the module and handles removal while the import is pending:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
var module = await JS.InvokeAsync<IJSObjectReference>("import", "/viewer.js");
if (_disposed) { await module.DisposeAsync(); return; }
_module = module;
await module.InvokeVoidAsync("mount", _host);
}
The complete Blazor sample includes the host markup, fields, interactive server configuration and module disposal. Its browser module opens local files without uploading them and releases the document when the host is removed.
Analyze in Node
Save as analyze.mjs and run node analyze.mjs document.docx. No browser is required.
import { readFile } from "node:fs/promises";
import { openDocument } from "@onodocs/sdk";
const doc = await openDocument(await readFile(process.argv[2]));
const paragraphs = doc.query.paragraphs().map(paragraph => paragraph.text);
console.log(paragraphs);
console.log(`${doc.query.tables().count()} tables`);The same query API works in the browser. For source facts and document structure, use doc.source and doc.semantic. See document properties.
Render on the server
Server rendering requires playwright-core and a separately installed Chrome or Chromium:
npm install @onodocs/sdk playwright-coreSave as render.mjs and run node render.mjs document.docx.
import { readFile, writeFile } from "node:fs/promises";
import { createRenderer } from "@onodocs/sdk/server";
const renderer = await createRenderer({ channel: "chrome" });
try {
const doc = await renderer.openDocument(await readFile(process.argv[2]));
try {
await writeFile("page-1.png", await doc.renderPage(0, { dpi: 144 }));
await writeFile("document.pdf", await doc.pdf({ dpi: 150 }));
} finally { await doc.dispose(); }
} finally { await renderer.dispose(); }Reuse a renderer across requests in a service. PDFs include searchable text, links, bookmarks and supported tagged structure. Supported graphics remain vector; visible native text is rasterized. This is not a PDF/UA certification. See server options and lifecycle.
Process on the backend, display in the frontend
This sample opens a DOCX in Node.js and displays its pages in a browser. The server runs document processing in headless Chrome. The frontend uses only Canvas, with selection, copying and zoom.
The complete backend viewer on GitHub includes the server, frontend, a Word document and setup instructions. The server requires Node.js, Google Chrome and playwright-core, which the sample includes in its dependencies. Evaluation needs no licence key.
Define the endpoints
server.mjs serves the frontend and two document endpoints from one origin. GET /document returns the page list; GET /document/pages/0 returns the first page. Page numbers start at zero. The SDK prepares the response bodies when you request format: "json".
This is the document request handler. createServer comes from node:http, and doc is the opened server document. The full sample also serves the frontend assets and shuts down the renderer.
const server = createServer(async (request, response) => {
try {
const path = new URL(request.url, "https://example.invalid").pathname;
const match = /^\/document\/pages\/(\d+)$/.exec(path);
const index = match ? Number(match[1]) : -1;
if (path !== "/document" && (index < 0 || index >= doc.pages.length)) {
response.writeHead(404).end();
return;
}
const body = path === "/document"
? await doc.manifest({ format: "json" })
: await doc.page(index, { format: "json" });
response.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
response.end(body);
} catch {
response.writeHead(500).end("Unable to load the document.");
}
});
Open the document in the frontend
client.js opens the document URL. Canvas loads the page list and requests pages as they become visible.
import { openDocument } from "@onodocs/canvas";
const status = document.querySelector("#status");
let doc;
try {
doc = await openDocument("/document", {
container: document.querySelector("#pages"),
viewOptions: { zoom: "fit-width" }
});
window.addEventListener("pagehide", event => { if (!event.persisted) doc.dispose(); });
await doc.view.whenRendered();
status.textContent = `${doc.pages.length} pages. Select text to copy it.`;
} catch (error) {
doc?.dispose();
status.textContent = error.message;
}
Frontend HTML · Complete server.
Use it in your application
The sample listens only on your computer. In production, apply your application's document authorization to both endpoints. Same-origin session cookies work with the default client. For bearer tokens or cross-origin cookies, pass request: { headers, credentials } to Canvas openDocument; those options apply to the page list and every page request.
Keep the server document open while its endpoints are in use. For changing documents, give each completed version its own URL and reopen the frontend view after an update. Dispose the previous Canvas document. This keeps the page list and page content from different versions separate.
The original DOCX stays on the backend; the frontend receives the content needed to display and select its pages. Supply font files for consistent results across hosts, or ensure the required fonts are installed on both machines. See server options and Canvas options.
Fonts and deployment
Fonts affect line breaks and page count. Embedded document fonts load automatically. Provide additional fonts to match your documents.
const doc = await openDocument(file, {
fonts: { "Example Sans": "/fonts/example-sans.woff2" }
});Use licensed font URLs in the browser. Server document contexts make no outbound requests, so supply data URLs or install fonts on the rendering host. For offline use, keep scripts, documents, and fonts local.
Use the same browser version and fonts for consistent output. Import @onodocs/sdk/browser in browser bundles to keep server dependencies out. All entry points include TypeScript declarations.