TutorialMarch 16, 2026·2 min read

Next.js DOCX Editor: Edit Word Documents in the Browser

Add an open-source DOCX editor to your Next.js app. Client-side Word document editing with no server required. Includes live interactive demo.

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-editor

Add 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

ErrorFix
ReferenceError: document is not definedMissing ssr: false in dynamic()
Styles not renderingImport @eigenpal/docx-js-editor/styles.css in the client component
Module parse errorsAdd 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