TutorialMarch 20, 20263 min read

Astro DOCX editor: edit Word documents in an Astro site

Add a DOCX editor to an Astro project with a React island. Install the React adapter, load a .docx, edit it, and download the result.

To edit Word documents in an Astro site, add the React integration, install @docx-editor.dev/react, and render the editor as a client-only island. Astro renders pages on the server by default, and the editor needs a browser.

Live demo

Open your own .docx, or edit the sample document in the demo. The demo does not upload anything. The document stays in your browser.

Install

npx astro add react
npm install @docx-editor.dev/react @docx-editor.dev/core

@docx-editor.dev/core is a peer dependency. The adapter bundles the string catalog.

Build the editor component

The component uses the standard React component API. Astro hydrates it as a client-side island:

// src/components/DocxEditor.tsx
import { useState, useEffect, useRef, useCallback } from "react";
import { DocxEditor } from "@docx-editor.dev/react";
import type { DocxEditorRef } from "@docx-editor.dev/react";
import "@docx-editor.dev/core/styles/editor.css";
 
const DOCX_MIME =
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
 
export function Editor() {
  const editorRef = useRef<DocxEditorRef>(null);
  const [bytes, setBytes] = useState<Uint8Array>();
  const [fileName, setFileName] = useState("Untitled.docx");
 
  useEffect(() => {
    fetch("/sample.docx")
      .then((res) => res.arrayBuffer())
      .then((buf) => {
        setBytes(new Uint8Array(buf));
        setFileName("sample.docx");
      })
      // Nothing to load: leave `document` undefined and the editor opens empty.
      .catch(() => undefined);
  }, []);
 
  const handleSave = useCallback(async () => {
    const saved = await editorRef.current?.save();
    if (!saved) return;
    const blob = new Blob([saved], { type: DOCX_MIME });
    const url = URL.createObjectURL(blob);
    Object.assign(document.createElement("a"), {
      href: url,
      download: fileName,
    }).click();
    URL.revokeObjectURL(url);
  }, [fileName]);
 
  return (
    <div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
      <button onClick={handleSave}>Download .docx</button>
      <DocxEditor
        ref={editorRef}
        document={bytes}
        title={fileName}
        onTitleChange={setFileName}
      />
    </div>
  );
}

Use it in an Astro page

The client:only="react" directive skips server-side rendering (SSR) and renders the island in the browser. The editor uses window, document, and FileReader, so the directive is required:

---
// src/pages/editor.astro
import { Editor } from "../components/DocxEditor";
---
 
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>DOCX Editor</title>
  </head>
  <body>
    <Editor client:only="react" />
  </body>
</html>

Do not use client:load. It renders on the server first, which fails on the browser globals.

Configure Astro

// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
 
export default defineConfig({
  integrations: [react()],
});

Let users open a file

import { useState } from "react";
import { DocxEditor } from "@docx-editor.dev/react";
 
export function UploadEditor() {
  const [bytes, setBytes] = useState<Uint8Array>();
  const [fileName, setFileName] = useState("Untitled.docx");
 
  function handleUpload(event: React.ChangeEvent<HTMLInputElement>) {
    const file = event.target.files?.[0];
    if (!file) return;
    void file.arrayBuffer().then((buffer) => {
      setBytes(new Uint8Array(buffer));
      setFileName(file.name);
    });
  }
 
  return (
    <div style={{ height: "100vh" }}>
      <input type="file" accept=".docx" onChange={handleUpload} />
      {bytes && <DocxEditor document={bytes} title={fileName} />}
    </div>
  );
}

Save to an API endpoint

// src/pages/api/documents.ts
import type { APIRoute } from "astro";
 
export const POST: APIRoute = async ({ request }) => {
  const data = await request.arrayBuffer();
  // upload to S3, save to DB, etc.
  return new Response(JSON.stringify({ ok: true }), { status: 200 });
};
import { useRef } from "react";
import { DocxEditor, type DocxEditorRef } from "@docx-editor.dev/react";
 
export function SavingEditor({ bytes }: { bytes: Uint8Array }) {
  const editorRef = useRef<DocxEditorRef>(null);
 
  async function save() {
    const saved = await editorRef.current?.save();
    if (!saved) return;
    await fetch("/api/documents", { method: "POST", body: saved });
  }
 
  return (
    <>
      <button onClick={save}>Save</button>
      <DocxEditor ref={editorRef} document={bytes} />
    </>
  );
}

An API endpoint needs output: "server" or output: "hybrid" in your Astro config.

Common errors

ErrorFix
window is not defined at buildUse client:only="react", not client:load
Styles do not renderImport @docx-editor.dev/core/styles/editor.css inside the React component
React integration missingRun npx astro add react

Included features

The editor parses OOXML in the browser and renders the document as pages. It supports:

  • Bold, italic, underline, and strikethrough
  • Tables, including merged cells
  • Inline and floating images
  • Headers, footers, and page breaks
  • Zoom and a document outline

It writes the document back to .docx. The editor is Apache 2.0 and needs no document-processing server. For more information about tracked changes, comments, and custom nodes, see the Pro module.

Where to go next