Getting started
Install the package, mount the editor, and export the edited document, one step at a time.
Six steps from an empty React project to a document a person can edit and download.
Install the package
npm i @portone/docx-editorImport the stylesheet
import "@portone/docx-editor/styles.css";Once per application is enough, and the import carries the ProseMirror base styles the editor needs. Styling covers the custom properties that restyle the frame around the paper.
Get a document to open
document takes a File, a Blob, an ArrayBuffer, or a Uint8Array, so the file arrives
however your application already holds it.
A file input hands over a File directly:
<input
type="file"
accept=".docx"
onChange={(event) => setFile(event.target.files?.[0] ?? null)}
/>A document fetched from a server needs no more than its response body:
const response = await fetch("/agreement.docx");
const file = new File([await response.blob()], "agreement.docx");Bytes open immediately; a Blob or a File is read first, so the editor stands empty for that
moment.
Mount the editor
Render DocxEditor with the document, and keep a ref if the surrounding screen will export it:
import { DocxEditor, type DocxEditorHandle } from "@portone/docx-editor";
import { useRef } from "react";
export function Editor({ file }: { file: File }) {
const editorRef = useRef<DocxEditorHandle | null>(null);
return <DocxEditor ref={editorRef} document={file} />;
}The editor fills the height it is given, so put it in a box with one.
Export the edited document
downloadDocx takes the handle from the ref and saves the current editor state as a file.
It reports unavailable while no document is open yet and empty when there is nothing worth
exporting.
import {
DocxEditor,
type DocxEditorHandle,
downloadDocx,
} from "@portone/docx-editor";
import { useRef } from "react";
export function Editor({ file }: { file: File }) {
const editorRef = useRef<DocxEditorHandle | null>(null);
const download = () => {
const result = downloadDocx(editorRef.current, { fileName: file.name });
if (result.status === "exported") console.log(result.byteLength);
};
return (
<>
<button onClick={download} type="button">
Export .docx
</button>
<DocxEditor ref={editorRef} document={file} />
</>
);
}downloadDocx throws DocxExportError when the document cannot be written back safely, so wrap
the call when the surrounding screen should show the reason.
For the bytes without a browser download, call editorRef.current.exportBytes().
Check what you see
An opened document shows its own paper: the page size and margins it declares, its fonts, and its own styles, with approximate page boundaries drawn over the text and the built-in toolbar above it. Exporting and reopening the file in Word is the real check, since the round trip is what the editor is for.
If a refusal panel appears in place of the editor, the document was not opened, and the panel names the reason. Core API lists the codes behind those refusals.
Choose what the editor is for
mode decides what the editor offers.
{ kind: "readOnly" } takes no edits and therefore has no toolbar or context menus;
{ kind: "edit" } is the default, and its locking option adds the controls for settling part of
a document.
<DocxEditor
document={file}
mode={{ kind: "edit", locking: true }}
renderImportError={(error) => <p role="alert">{error.code}</p>}
/>Without renderImportError, a built-in panel naming the reason is drawn, so a refused document is
never silent either way.
Props lists the rest of the component's surface.
Next.js and other prerendering frameworks
The editor builds a ProseMirror view against the DOM, so it cannot render on the server. In a framework that prerenders, load it from a client component through a dynamic import with server rendering turned off, which is how this site mounts its own demo:
"use client";
import dynamic from "next/dynamic";
const Editor = dynamic(() => import("./Editor").then((m) => m.Editor), {
ssr: false,
});Fetch the document from inside that client component too, in an effect, and wrap the response in a
File as in the third step above.
The App Router also wants @portone/docx-editor in transpilePackages when it resolves to
TypeScript sources rather than to built output, which is the case for a workspace link.
Next
Read Features for what the editor edits and preserves, Props for the component's full surface, Custom controls to replace the built-in toolbar, and Core API for import and export without an editor.