Next.js
Set up a Next.js DOCX editor in the App Router. Dynamic import with ssr: false, the window is not defined fix, file upload, and saving back to .docx.
By the end of this page you have an App Router route that opens a .docx from disk, edits it in the browser, and downloads the result as a .docx.
Install
npm install @docx-editor.dev/react @docx-editor.dev/coreMount the editor
Two files. The route stays a thin client shell; the editor itself lives in a separate component loaded with dynamic() and ssr: false. This is the structure of examples/nextjs.
// app/page.tsx
'use client';
import dynamic from 'next/dynamic';
const Editor = dynamic(() => import('./components/Editor').then((m) => m.Editor), {
ssr: false,
loading: () => <div>Loading editor...</div>,
});
export default function Page() {
return <Editor />;
}// app/components/Editor.tsx
'use client';
import { useRef, useState } from 'react';
import { DocxEditor, type DocxEditorRef } from '@docx-editor.dev/react';
import '@docx-editor.dev/core/styles/editor.css';
export function Editor() {
const editorRef = useRef<DocxEditorRef>(null);
const [buffer, setBuffer] = useState<ArrayBuffer | null>(null);
const [fileName, setFileName] = useState('document.docx');
async function onFileSelect(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
setBuffer(await file.arrayBuffer());
setFileName(file.name);
}
async function onSave() {
const out = await editorRef.current?.save();
if (!out) return;
const blob = new Blob([out], {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
URL.revokeObjectURL(url);
}
return (
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: 8, display: 'flex', gap: 8 }}>
<input type="file" accept=".docx" onChange={onFileSelect} />
<button onClick={onSave}>Save .docx</button>
</div>
<div style={{ flex: 1, minHeight: 0 }}>
<DocxEditor ref={editorRef} document={buffer ?? undefined} />
</div>
</div>
);
}Leaving document undefined mounts an empty editor until the user picks a file. The stylesheet import is required once, and the editor fills its parent so it needs a box with a real height.
Why this pattern
The editor reads the DOM and measures text at mount, so it cannot run during server rendering. If you import DocxEditor directly in a route, next build fails during prerender with:
ReferenceError: window is not defineddynamic(..., { ssr: false }) is the fix: the editor module is never evaluated on the server,
and it stays out of the route's initial bundle. Keep the rest of the page (nav, footer) as server
components; only the editor needs the client boundary.
Run the example
The example resolves @docx-editor.dev/* from the monorepo workspace, so build the packages once first:
git clone https://github.com/eigenpal/docx-editor.git
cd docx-editor
bun install
bun run build:packages
bun run dev:nextjs # http://localhost:3000Next steps
- Quickstart: the load, edit, save flow in detail
- Loading and saving: URLs, autosave, upload to your API
- Next.js DOCX editor guide: the long-form walkthrough
- Next.js example on GitHub
Installation
Install the DOCX editor in React, Next.js, Vite, Remix, or Astro. Covers the stylesheet import, the client-only SSR boundary, fonts, and which package to pick.
Vite
Build a Vite DOCX editor with React. Install the package, mount the component, open a .docx from disk, edit it, and download the edited result.