Live demo
Upload a .docx or edit the sample below. Nothing leaves your browser.
Install
npx astro add react
npm install @eigenpal/docx-js-editorEditor component
Standard React component. Astro hydrates it as an island on the client:
// src/components/DocxEditor.tsx
import { useState, useEffect, useRef, useCallback } from "react";
import {
DocxEditor,
createEmptyDocument,
} from "@eigenpal/docx-js-editor";
import type { DocxEditorRef, Document } from "@eigenpal/docx-js-editor";
import "@eigenpal/docx-js-editor/styles.css";
export function Editor() {
const editorRef = useRef<DocxEditorRef>(null);
const [documentBuffer, setDocumentBuffer] = useState<ArrayBuffer | null>(null);
const [currentDocument, setCurrentDocument] = useState<Document | null>(null);
const [fileName, setFileName] = useState("sample.docx");
useEffect(() => {
fetch("/sample.docx")
.then((res) => res.arrayBuffer())
.then((buf) => {
setDocumentBuffer(buf);
setFileName("sample.docx");
})
.catch(() => {
setCurrentDocument(createEmptyDocument());
setFileName("Untitled.docx");
});
}, []);
const handleSave = useCallback(async () => {
const saved = await editorRef.current?.save();
if (!saved) return;
const blob = new Blob([saved], {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
});
const url = URL.createObjectURL(blob);
Object.assign(document.createElement("a"), {
href: url,
download: fileName,
}).click();
URL.revokeObjectURL(url);
}, [fileName]);
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
<DocxEditor
ref={editorRef}
document={documentBuffer ? undefined : currentDocument}
documentBuffer={documentBuffer}
showToolbar
showRuler
showZoomControl
/>
</div>
);
}Use it in an Astro page
client:only="react" skips SSR and renders only in the browser. The editor uses window, document, and FileReader so this is required:
---
// src/pages/editor.astro
import { Editor } from "../components/DocxEditor";
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DOCX Editor</title>
</head>
<body>
<Editor client:only="react" />
</body>
</html>Don't use client:load. It attempts SSR first and fails on browser globals.
Astro config
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
export default defineConfig({
integrations: [react()],
});File uploads
function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
file.arrayBuffer().then((buf) => {
setDocumentBuffer(buf);
setFileName(file.name);
});
}Save to an API endpoint
// src/pages/api/documents.ts
import type { APIRoute } from "astro";
export const POST: APIRoute = async ({ request }) => {
const data = await request.arrayBuffer();
// upload to S3, save to DB, etc.
return new Response(JSON.stringify({ ok: true }), { status: 200 });
};const saved = await editorRef.current?.save();
await fetch("/api/documents", { method: "POST", body: saved });Requires output: "server" or output: "hybrid" in your Astro config.
Common errors
| Error | Fix |
|---|---|
window is not defined at build | Use client:only="react", not client:load |
| Styles not rendering | Import @eigenpal/docx-js-editor/styles.css inside the React component |
| React integration missing | Run npx astro add react |
What you get
The editor parses OOXML on the client and renders via ProseMirror. Out of the box: bold/italic/underline, tables with cell merging, inline images, headers and footers, page breaks, tracked changes, threaded comments, zoom, and document outline. It exports back to valid .docx. MIT licensed, ~200KB gzipped, no server dependency.
Next steps
- Astro example on GitHub
- Track changes and comments for review workflows
- Document templates with variable placeholders
- Full docs for all props and config