Live demo
Upload a .docx or edit the sample below. Nothing leaves your browser.
Install
npm install @eigenpal/docx-js-editorEditor component
The editor uses browser APIs so it needs to run client-only. Separate file:
// app/components/DocxEditor.tsx
import { useState, useEffect, useRef, useCallback } from "react";
import {
DocxEditor,
createEmptyDocument,
} from "@eigenpal/docx-js-editor";
import type { DocxEditorRef, Document } from "@eigenpal/docx-js-editor";
import "@eigenpal/docx-js-editor/styles.css";
export function Editor() {
const editorRef = useRef<DocxEditorRef>(null);
const [documentBuffer, setDocumentBuffer] = useState<ArrayBuffer | null>(null);
const [currentDocument, setCurrentDocument] = useState<Document | null>(null);
const [fileName, setFileName] = useState("sample.docx");
useEffect(() => {
fetch("/sample.docx")
.then((res) => res.arrayBuffer())
.then((buf) => {
setDocumentBuffer(buf);
setFileName("sample.docx");
})
.catch(() => {
setCurrentDocument(createEmptyDocument());
setFileName("Untitled.docx");
});
}, []);
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: fileName,
}).click();
URL.revokeObjectURL(url);
}, [fileName]);
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
<DocxEditor
ref={editorRef}
document={documentBuffer ? undefined : currentDocument}
documentBuffer={documentBuffer}
showToolbar
showRuler
showZoomControl
/>
</div>
);
}Lazy-load in your route
Remix routes run on both server and client. React.lazy + Suspense keeps the editor client-only:
// app/routes/_index.tsx
import type { MetaFunction } from "@remix-run/node";
import { lazy, Suspense, useEffect, useState } from "react";
export const meta: MetaFunction = () => [
{ title: "DOCX Editor — Remix" },
{ name: "description", content: "Edit Word documents in the browser" },
];
const Editor = lazy(() =>
import("../components/DocxEditor").then((m) => ({ default: m.Editor }))
);
export default function Index() {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) {
return <div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100vh", color: "#666" }}>Loading DOCX Editor...</div>;
}
return (
<Suspense fallback={<div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100vh", color: "#666" }}>Loading DOCX Editor...</div>}>
<Editor />
</Suspense>
);
}The mounted check prevents hydration mismatches.
File uploads
function handleUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
if (!file) return;
file.arrayBuffer().then((buf) => {
setDocumentBuffer(buf);
setFileName(file.name);
});
}Save via Remix action
// app/routes/api.documents.tsx
import type { ActionFunctionArgs } from "@remix-run/node";
export async function action({ request }: ActionFunctionArgs) {
const data = await request.arrayBuffer();
// upload to S3, save to DB, etc.
return Response.json({ ok: true });
}const saved = await editorRef.current?.save();
await fetch("/api/documents", { method: "POST", body: saved });Common errors
| Error | Fix |
|---|---|
| Hydration mismatch | Wrap the editor in a mounted check as shown above |
| Styles not rendering | Import @eigenpal/docx-js-editor/styles.css in the Editor component, not in the route |
window is not defined | Use React.lazy. Don't import the editor at the top of a route file |
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
- Remix example on GitHub
- Track changes and comments for review workflows
- Document templates with variable placeholders
- Full docs for all props and config