Live demo
This editor runs in the Next.js app you're reading right now. Upload a .docx or edit the sample below. Nothing leaves your browser.
The SSR problem
Next.js server-renders by default. A DOCX editor needs browser APIs: FileReader, Blob, DOM manipulation. The fix is dynamic() with ssr: false, but you also need transpilePackages or the ESM imports will break at build time.
Install
npm install @eigenpal/docx-js-editorAdd to next.config.ts:
const nextConfig = {
transpilePackages: ["@eigenpal/docx-js-editor"],
};Editor component
"use client";
import { useState, useEffect, useRef, useCallback } from "react";
import { DocxEditor } from "@eigenpal/docx-js-editor";
import type { DocxEditorRef } from "@eigenpal/docx-js-editor";
import "@eigenpal/docx-js-editor/styles.css";
export function MyDocxEditor() {
const editorRef = useRef<DocxEditorRef>(null);
const [buffer, setBuffer] = useState<ArrayBuffer | null>(null);
useEffect(() => {
fetch("/sample.docx")
.then((res) => res.arrayBuffer())
.then(setBuffer);
}, []);
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: "edited.docx",
}).click();
URL.revokeObjectURL(url);
}, []);
if (!buffer) return <div>Loading...</div>;
return (
<div style={{ height: "80vh" }}>
<DocxEditor
ref={editorRef}
documentBuffer={buffer}
showToolbar
showRuler
showZoomControl
/>
<button onClick={handleSave}>Download .docx</button>
</div>
);
}"use client" is required. The component uses refs, hooks, and browser globals.
Load it in a page
import dynamic from "next/dynamic";
const DocxEditor = dynamic(
() => import("@/components/DocxEditor").then((m) => m.MyDocxEditor),
{ ssr: false },
);
export default function EditorPage() {
return <DocxEditor />;
}The page shell is server-rendered for SEO. The editor mounts after hydration.
File uploads
function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => setBuffer(reader.result as ArrayBuffer);
reader.readAsArrayBuffer(file);
}Swap the buffer and the editor re-renders with the new document. No reload.
Save to API route
// app/api/documents/route.ts
export async function POST(req: NextRequest) {
const data = await req.arrayBuffer();
// upload to S3, save to DB, etc.
return NextResponse.json({ ok: true });
}const saved = await editorRef.current?.save();
await fetch("/api/documents", { method: "POST", body: saved });Common errors
| Error | Fix |
|---|---|
ReferenceError: document is not defined | Missing ssr: false in dynamic() |
| Styles not rendering | Import @eigenpal/docx-js-editor/styles.css in the client component |
| Module parse errors | Add transpilePackages to next.config.ts |
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
- Next.js example on GitHub
- Track changes and comments for review workflows
- Document templates with variable placeholders
- Full docs for all props and config