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.

This guide shows 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 client route shell; the editor itself loads from a separate component 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 ?? 'blank'} />
      </div>
    </div>
  );
}

document={buffer ?? 'blank'} starts the user on an empty page, and replaces it with their file when they pick one. Leaving document undefined is not the same thing. It means no document at all. The editor shows its loading screen, and every control reports no document is loaded until bytes arrive. Use undefined only while your fetch is still running.

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