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
| Error | Fix |
|---|---|
window is not defined at build | Use client:only="react", not client:load |
| Styles do not render | Import @docx-editor.dev/core/styles/editor.css inside the React component |
| React integration missing | Run 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
- Astro setup reference covers the same steps in the docs.
- Astro example on GitHub is a runnable project.
- Tracked changes and Comments cover review workflows.
- React props reference documents every prop.