Frameworks

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/core

Mount 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 defined

dynamic(..., { 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:3000

Next steps

On this page