Free
- Open and save .docx files
- Edit text, tables, images, links, and lists
- Use the packaged React or Vue editor
- Ship commercial products
Use it in commercial products.
Open-source WYSIWYG .docx editor for React and Vue. Tracked changes, collaboration, and customizable UI, with canonical OOXML out.
Edit .docx documents in the browser. Parsing, rendering, and editing all run client-side.
.docx edited by code. Headless over bytes on a server, or in a page against a document your user already has open. Office.js-compatible, so code you wrote for a Word add-in runs here.
Suggesting mode wraps every edit in revision markup with author attribution, and threaded comments anchor to text ranges. Accept, reject, reply, and resolve from the sidebar, the way Word does it.
Pixel-perfect OOXML rendering: fonts, colors, bold, italic, highlights, plus inline and floating images with positioning and text wrap.
The editor rewrites what it understands and preserves the rest byte-for-byte, so a document you did not author survives a save. CI gates it with a canonical tree fingerprint and a save-and-reopen digest.
Edit one .docx together with live cursors, presence, and personal undo. Comments and tracked changes stay in sync, and offline edits merge after reconnection.
Use DocxEditor as it comes, or assemble Root, Viewport, and Content yourself. The primitives and hooks are public, so a button you write runs the same commands as the built-in toolbar.
Two adapters over one engine. @docx-editor.dev/react and @docx-editor.dev/vue take the same props, run the same commands, and expose the same composition primitives, so a document behaves identically in either.
Built-in internationalization for any language. Localize every toolbar label, tooltip, and UI string. Community contributions welcome.
An Office.js compatible API for editing .docx documents. Queue changes in code, then sync() to apply them in one ordered, atomic operation.
import { DocxEditor } from '@docx-editor.dev/editor-api';
const runtime = await DocxEditor.createServer(bytes, {
author: 'Review bot',
});
await runtime.run(async (context) => {
const matches = context.document.body.search('$50k');
matches.load();
await context.sync(); // one round trip: now you know what matched
for (const m of matches.items) m.insertText('$500k', 'Replace');
await context.sync(); // one atomic batch: all of the writes, or none
});
// In a page? Swap createServer for createBrowser(editor).Read bytes, batch your edits against paragraphs and ranges, then save. Headless in Node, with no browser and no editor mounted, so a script or an agent can drive it.
Read the tutorial →GuideThe same calls, against the document your user already has open. Put them behind your agent's tools and its edits land in the live editor, visible as they happen.
Read the guide →The editor API gives a model the document your reader has open. It searches for a phrase, writes at that anchor, and syncs the change into the live editor. Everything runs in the browser.
A custom node is an inline run that carries your own typed data. You give it a schema and the chrome it draws with. Word stores it as a content control, so it survives the round trip.
Section 2 · Consensus
Decentralised consensus removes the need for a trusted third party, which is the property the protocol depends on.
import { tool } from 'ai';
import { z } from 'zod';
import { useDocxEditor } from '@docx-editor.dev/react';
import { DocxEditor } from '@docx-editor.dev/editor-api/browser';
// Borrows the editor your reader already has open.
export function useCitationTool() {
const editor = useDocxEditor();
return tool({
description: 'Cite a source for a claim.',
inputSchema: z.object({ search: z.string(), sourceId: z.string() }),
execute: ({ search, sourceId }) =>
DocxEditor.createBrowser(editor!).run(async (context) => {
const hit = context.document.body.search(search).getFirst();
hit.insertText(` (${sourceId})`, 'End');
await context.sync(); // lands in the live editor
}),
});
}<script setup lang="ts">
import { tool } from 'ai';
import { z } from 'zod';
import { useDocxEditor } from '@docx-editor.dev/vue';
import { DocxEditor } from '@docx-editor.dev/editor-api/browser';
// Borrows the editor your reader already has open. Vue answers a ref.
const editor = useDocxEditor();
const citation = tool({
description: 'Cite a source for a claim.',
inputSchema: z.object({ search: z.string(), sourceId: z.string() }),
execute: ({ search, sourceId }) =>
DocxEditor.createBrowser(editor.value!).run(async (context) => {
const hit = context.document.body.search(search).getFirst();
hit.insertText(` (${sourceId})`, 'End');
await context.sync(); // lands in the live editor
}),
});
</script>import { z } from 'zod';
import { customNodesModule, defineCustomNode } from '@docx-editor.dev/pro';
import { CustomNodeChrome } from '@docx-editor.dev/pro/react';
import { DocxEditor } from '@docx-editor.dev/react';
export const Citation = defineCustomNode({
name: 'citation',
tagPrefix: 'acme',
chrome: { color: '#6b3fb8' },
schema: z.object({ author: z.string(), year: z.number() }),
// What the page reads as, derived from the payload — so the
// sentence can never drift from the source behind it.
text: (d) => `(${d.author} ${d.year})`,
});
const MODULES = [customNodesModule({ nodes: [Citation] })];
export function Editor({ bytes }: { bytes: Uint8Array }) {
return (
<DocxEditor.Root document={bytes} modules={MODULES}>
<DocxEditor.Viewport>
<DocxEditor.Content />
<CustomNodeChrome onNodeClick={(n) => open(Citation.dataOf(n))} />
</DocxEditor.Viewport>
</DocxEditor.Root>
);
}<script setup lang="ts">
import { z } from 'zod';
import { customNodesModule, defineCustomNode } from '@docx-editor.dev/pro';
import { CustomNodeChrome } from '@docx-editor.dev/pro/vue';
import {
DocxEditorContent,
DocxEditorRoot,
DocxEditorViewport,
} from '@docx-editor.dev/vue';
defineProps<{ bytes: Uint8Array }>();
const Citation = defineCustomNode({
name: 'citation',
tagPrefix: 'acme',
chrome: { color: '#6b3fb8' },
schema: z.object({ author: z.string(), year: z.number() }),
// What the page reads as, derived from the payload — so the
// sentence can never drift from the source behind it.
text: (d) => `(${d.author} ${d.year})`,
});
const modules = [customNodesModule({ nodes: [Citation] })];
</script>
<template>
<DocxEditorRoot :document="bytes" :modules="modules">
<DocxEditorViewport>
<DocxEditorContent />
<CustomNodeChrome :on-node-click="(n) => open(Citation.dataOf(n))" />
</DocxEditorViewport>
</DocxEditorRoot>
</template>Use the Apache 2.0 editor in commercial products at no charge. Pro adds tracked changes, comments, real-time collaboration, custom nodes, and the document automation API.
Use it in commercial products.
Cancel any time. Your Pro license ends with the paid month.
For teams that want a direct line to the engineers who build the editor.
We'll put the support scope and response times in your contract.
Pro and Pro + Priority include the same software. Priority adds a private Slack channel and engineering support. Read the Licensing Terms.
Mount the editor, hand it a buffer, call save. Same flow in React and Vue.
// Editor.tsx
import { useRef } from 'react';
import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/react';
import '@docx-editor.dev/core/styles/editor.css';
export function Editor({ file }: { file: ArrayBuffer }) {
const editorRef = useRef<DocxEditorRef>(null);
async function save() {
const buffer = await editorRef.current?.save();
if (!buffer) return;
await fetch('/api/documents/1', { method: 'PUT', body: buffer });
}
return (
<>
<button onClick={save}>Save</button>
<DocxEditor ref={editorRef} document={file} />
</>
);
}Framework examples with full source code, for both the React and the Vue adapter.
Start with the React Word editor guide or the Vue DOCX editor guide.
No server/client boundary, so the editor mounts as-is.
View example →App Router, in a client component. Nothing to SSR.
View example →A client-only route with a lazy import.
View example →A React island behind client:only, which skips SSR.
View example →The Vue 3 adapter, mounted straight into a Vite app.
View example →A .client.vue component, which Nuxt keeps out of SSR.
View example →docx-editor is an open-source WYSIWYG document editor for React and Vue 3 that lets you edit .docx files in the browser. It parses and renders OOXML documents with full fidelity, including tables, images, headers, footers, and formatting. No server-side processing required.
Yes. The editor is released under the Apache 2.0 license. You can use it in personal and commercial projects, modify the source code, and distribute it freely, with no usage limits and no watermarks. Two add-on packages are commercially licensed: @docx-editor.dev/pro (tracked changes, comments, custom nodes) and @docx-editor.dev/editor-api (document automation). Both are free to evaluate, and production use needs an agreement.
Two adapters over one engine. @docx-editor.dev/react works with Vite, Next.js, Remix, Astro, and anything else that runs React. @docx-editor.dev/vue covers Vue 3 and Nuxt. The repository has runnable examples for each. Mount it client-side either way: the editor lays out and paints the document in the browser, so there is nothing to server-render.
Yes. @docx-editor.dev/vue is the Vue 3 adapter, and it takes the same props and runs the same commands as the React one. Install it with @docx-editor.dev/core and import @docx-editor.dev/vue/styles.css once, then render <DocxEditor :document="bytes" />. Composables (useDocxEditor, useEditorState, useEditorCommand) come from the package root. See the Vue docs and the Vite with Vue setup.
Yes, through @docx-editor.dev/vue. The editor needs the DOM at mount, so keep it out of SSR: name the component with a .client.vue suffix, add the stylesheet to css in nuxt.config.ts, and prebundle the adapter and engine with vite.optimizeDeps.include. The Nuxt setup page has the full config.
Yes. @docx-editor.dev/react works in Next.js App Router and Pages Router. The component is client-only (it needs the DOM at mount, and rendering it during SSR throws window is not defined), so wrap with 'use client' or next/dynamic with ssr: false. Tracked changes, comments, and content controls all run in the browser. See the Next.js post on the blog for a full walkthrough.
No. Everything runs client-side: parsing, rendering, editing. Zero backend dependencies. Just install the package, pass a buffer, and you have a working editor. No infra to maintain.
Yes. The live editor at /editor runs the same @docx-editor.dev/react build you'd install locally. Open a .docx, edit, save back, all client-side.
Yes, through `@docx-editor.dev/pro`. Several people can edit one DOCX with live presence and personal undo. Comments, tracked changes, and review decisions stay synchronized. Use WebRTC, Hocuspocus, or an existing Yjs provider.
Yes, through @docx-editor.dev/pro. Register the review module on the editor root and suggesting mode wraps every edit in Word revision markup with author attribution. Accept or reject changes one at a time or in bulk.
Yes, through @docx-editor.dev/pro. Select text, leave a threaded comment, reply, and resolve. Threads anchor to a range and round-trip as Word comments.
Yes. @docx-editor.dev/i18n ships ten locales: English, German, French, Hebrew, Hindi, Indonesian, Polish, Brazilian Portuguese, Turkish, and Simplified Chinese. Pass a t resolver to <DocxEditor> and every toolbar label, tooltip, and dialog string follows it. Community contributions for new locales are welcome; see the translations docs for how to contribute.
Open a real document in the editor, then wire it into your app. It all runs client-side.