Paste, file import, and file download use browser APIs. Keep these operations in client code. Export uses React static rendering, so pass the descriptors and static components that define your document output.
The app-local DocxKit installs WordPastePlugin. File import and export are standalone functions.
'use client';
import { WordPastePlugin } from 'platejs/docx/paste';
export const DocxKit = [WordPastePlugin] as const;'use client';
import { WordPastePlugin } from 'platejs/docx/paste';
export const DocxKit = [WordPastePlugin] as const;import { createEditor } from "platejs/react";
import { DocxKit } from "@/components/editor/docx";
const editor = createEditor({
plugins: [...DocxKit],
});import { createEditor } from "platejs/react";
import { DocxKit } from "@/components/editor/docx";
const editor = createEditor({
WordPastePlugin inlines pasted CSS and normalizes Word HTML and RTF before the installed Plate codecs decode the content.
import { WordPastePlugin } from "platejs/docx/paste";
import { createEditor } from "platejs/react";
const editor = createEditor({
plugins: [WordPastePlugin],
});import { WordPastePlugin } from "platejs/docx/paste";
import { createEditor } from "platejs/react";
const editor = createEditor({
plugins: [WordPastePlugin],
});Clipboard paste does not upload embedded images or preserve Word table geometry. Configure those destination policies in their owning media and table features.
importDocx reads a Blob or ArrayBuffer without modifying the editor. On success, replace the complete editor value so document metadata, named roots, and authored review state stay coherent.
Inside a mounted React control, get that complete editor with
useModelEditor(). useEditor() returns the provider-selected mounted command
view, whose root and authored projection belong to that view.
import { importDocx } from "platejs/docx/import";
import { createEditor } from "platejs/react";
import { BaseEditorKit } from "@/components/editor/plugins-static";
const editor = createEditor({
plugins: BaseEditorKit,
});
export async function importFile(file: File) {
const result = await importDocx(editor, file);
if (!result.ok) {
return { diagnostics: result.diagnostics };
}
editor.update.value.replace(result.document);
return {
comments: result.comments,
diagnostics: result.diagnostics,
};
}import { importDocx } from "platejs/docx/import";
import { createEditor } from "platejs/react";
import { BaseEditorKit } from "@/components/editor/plugins-static";
const editor = createEditor({
plugins: BaseEditorKit,
});
export async function importFile(file: File) {
const result = await importDocx(editor, file);
if (!result.ok) {
return { diagnostics: result.diagnostics };
}
editor.update.value.replace(result.document);
return {
An expected file, package, limit, or decode failure returns { ok: false, diagnostics } and no document. An aborted import rejects with the signal reason. Invalid options and internal invariant failures throw.
Set retainSource: true when the same editor imports, edits, and exports a Word file. A successful import includes a DocxSource. Keep that object in application state, pass it to exportToDocx, and dispose it when the document closes or another file replaces it.
import { exportToDocx } from "platejs/docx/export";
import { importDocx } from "platejs/docx/import";
const imported = await importDocx(editor, file, {
retainSource: true,
});
if (!imported.ok) return imported.diagnostics;
editor.update.value.replace(imported.document);
const exported = await exportToDocx(editor, {
editorPlugins,
projection: "review",
source: imported.source,
stylesheet,
});
imported.source.dispose();import { exportToDocx } from "platejs/docx/export";
import { importDocx } from "platejs/docx/import";
const imported = await importDocx(editor, file, {
retainSource: true,
});
if (!imported.ok) return imported.diagnostics;
editor.update.value.replace(imported.document);
const exported = await exportToDocx(editor, {
editorPlugins,
projection: "review",
source: imported.source,
stylesheet,
});
imported.source.dispose();DocxSource retains the admitted compressed package plus immutable document, comment, schema, and limit correspondence. It does not retain expanded ZIP entries or an editor reference. dispose() is idempotent. An export that already acquired the source can finish after disposal; later exports report source-unavailable and generate a fresh DOCX.
The importer returns comment format facts without writing app comment storage. Each DocxComment can include a rich Plate body, an exact target.range in proposed-projection coordinates, Word author and date metadata, a durable ID, a parent ID, and resolved state. Map those records to your app identities explicitly.
Supported Word insertions, deletions, moves, run property changes, and paragraph property changes become authored changes. Revision IDs follow Word source order and structural dependencies; dates remain metadata. A missing or invalid date uses the authored unknown-time value and produces a diagnostic.
If a revision cannot map to the installed schema, the importer keeps the Word-visible proposed content and reports the loss in diagnostics.
exportToDocx captures the editor once before asynchronous rendering. Pass the visible projection explicitly.
import { exportToDocx } from "platejs/docx/export";
import { createEditor } from "platejs/react";
import { BaseEditorKit } from "@/components/editor/plugins-static";
import {
DOCX_EXPORT_STYLES,
DocxExportKit,
} from "@/components/editor/docx-export";
const editor = createEditor({ plugins: BaseEditorKit });
export async function downloadEditorDocx() {
const result = await exportToDocx(editor, {
editorPlugins: [...BaseEditorKit, ...DocxExportKit],
orientation: "portrait",
projection: "proposed",
stylesheet: DOCX_EXPORT_STYLES,
title: "Document",
});
if (!result.ok) return result.diagnostics;
const url = URL.createObjectURL(result.blob);
const link = document.createElement("a");
link.href = url;
link.download = "document.docx";
document.body.append(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
return result.diagnostics;
}import { exportToDocx } from "platejs/docx/export";
import { createEditor } from "platejs/react";
import { BaseEditorKit } from "@/components/editor/plugins-static";
import {
DOCX_EXPORT_STYLES,
DocxExportKit,
} from "@/components/editor/docx-export";
const editor = createEditor({ plugins: BaseEditorKit });
export async function downloadEditorDocx() {
const result = await exportToDocx(editor, {
editorPlugins: [...BaseEditorKit, ...DocxExportKit],
orientation: "portrait",
projection: "proposed",
Choose accepted to omit pending changes, proposed to apply them, or review to write Word tracked-change markup. Accepted and proposed export report omitted authored changes. Review export also writes a correspondence-bound native envelope for exact unchanged Plate round trips.
The importer always converts the Word-visible package before considering that envelope. It restores native metadata and named roots only when the envelope version, installed schema, package-part digests, and accepted/proposed Word projections all match. An edited or corrupt package falls back to the Word-derived document with a native-data-ignored diagnostic.
With a valid source, an unchanged review export returns the admitted file byte for byte. Exact reuse requires the same compiled schema, an unchanged review document, unchanged or omitted comments, and no title, margins, orientation, or pageSize override. Static renderer options are bypassed on this path because no content is regenerated.
If an exact check fails, exportToDocx regenerates the complete document body. It can copy these source units when their internal relationships are closed, reachable, nonconflicting, and free of active or external content:
The regenerated package omits source body XML, body-only relationships, orphan parts, multiple-section headers and footers, external relationships, digital signatures, macros, ActiveX, OLE objects, and controls. Diagnostics report why the source was rewritten and identify each omitted source part. Exact byte reuse can return an unchanged admitted file that contains those units because it does not create a new package claim.
When regeneration is required, omitted comments means that no comments are generated. Pass the current app-owned comment set explicitly to retain comments after an edit. Imported comment ranges are not relocated automatically.
Pass comments whose ranges address the editor's proposed projection. Export maps those ranges into accepted or review output before rendering:
const result = await exportToDocx(editor, {
comments,
editorPlugins: [...BaseEditorKit, ...DocxExportKit],
projection: "review",
});const result = await exportToDocx(editor, {
comments,
editorPlugins: [...BaseEditorKit, ...DocxExportKit],
projection: "review",
});The same configured static renderer handles the main document and rich comment bodies. Comments without a complete range remain omitted with a diagnostic.
The app-local DocxExportKit supplies static renderers for code blocks, columns, equations, callouts, headings, and tables of contents. DOCX_EXPORT_STYLES supplies the copied presentation preset.
import { BaseHeadingPlugin, BaseCodeBlockPlugin } from 'platejs';
import { BaseCalloutPlugin } from 'platejs/callout';
import { BaseColumnItemPlugin, BaseColumnPlugin } from 'platejs/layout';
import { BaseEquationPlugin, BaseInlineEquationPlugin } from 'platejs/math';
import { BaseTocPlugin } from 'platejs/toc';
import { CalloutElementDocx } from '@/components/editor/callout-static';
import { CodeBlockElementDocx } from '@/components/editor/code-block-static';
import {
ColumnElementDocx,
ColumnGroupElementDocx,
} from '@/components/editor/column-static';
import { HeadingElementDocx } from '@/components/editor/heading-static';
import {
EquationElementDocx,
InlineEquationElementDocx,
} from '@/components/editor/math-static';
import { TocElementDocx } from '@/components/editor/toc-static';
export const DOCX_EXPORT_STYLES = `
body {
font-family: 'Calibri', 'Arial', sans-serif;
font-size: 11pt;
line-height: 1.5;
color: #000;
margin: 0;
padding: 20px;
}
h1 { font-size: 24pt; font-weight: bold; margin: 0 0 12pt 0; }
h2 { font-size: 18pt; font-weight: bold; margin: 0 0 10pt 0; }
h3 { font-size: 14pt; font-weight: bold; margin: 0 0 8pt 0; }
h4 { font-size: 12pt; font-weight: bold; margin: 0 0 6pt 0; }
h5 { font-size: 11pt; font-weight: bold; margin: 0 0 6pt 0; }
h6 { font-size: 10pt; font-weight: bold; margin: 0 0 6pt 0; }
p { margin: 0 0 8pt 0; }
ul, ol { margin: 0 0 8pt 0; padding-left: 20pt; }
li { margin: 0 0 4pt 0; }
strong, b { font-weight: bold; }
em, i { font-style: italic; }
u { text-decoration: underline; }
s, strike, del { text-decoration: line-through; }
code {
font-family: 'Courier New', Consolas, monospace;
background-color: #f5f5f5;
padding: 2px 4px;
border-radius: 3px;
}
pre {
font-family: 'Courier New', Consolas, monospace;
background-color: #f5f5f5;
padding: 10px;
margin: 0 0 8pt 0;
white-space: pre-wrap;
border-radius: 4px;
}
.hljs-addition, .hljs-name, .hljs-quote, .hljs-selector-pseudo, .hljs-selector-tag { color: #22863a; }
.hljs-attr, .hljs-attribute, .hljs-literal, .hljs-meta, .hljs-number, .hljs-operator,
.hljs-section, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-id, .hljs-variable { color: #005cc5; }
.hljs-built_in, .hljs-symbol { color: #e36209; }
.hljs-bullet { color: #735c0f; }
.hljs-comment, .hljs-formula { color: #6a737d; }
.hljs-deletion { color: #b31d28; }
.hljs-doctag, .hljs-keyword, .hljs-template-tag, .hljs-template-variable, .hljs-type { color: #d73a49; }
.hljs-regexp, .hljs-string { color: #032f62; }
.hljs-title { color: #6f42c1; }
.hljs-emphasis { font-style: italic; }
.hljs-section, .hljs-strong { font-weight: bold; }
blockquote {
border-left: 3px solid #ccc;
margin: 0 0 8pt 0;
padding-left: 10pt;
color: #666;
font-style: italic;
}
table {
border-collapse: collapse;
width: 100%;
margin: 0 0 8pt 0;
}
th, td {
border: 1px solid #ccc;
padding: 6pt;
text-align: left;
}
th {
background-color: #f5f5f5;
font-weight: bold;
}
a {
color: #0066cc;
text-decoration: underline;
}
img {
max-width: 100%;
height: auto;
}
hr {
border: none;
border-top: 1px solid #ccc;
margin: 12pt 0;
}
sup { vertical-align: super; font-size: 8pt; }
sub { vertical-align: sub; font-size: 8pt; }
mark { background-color: #ffff00; }
`.trim();
/**
* Editor kit for DOCX export.
*
* Uses standard static components for most elements (with juice CSS inlining),
* but uses docx-specific components for elements that need special handling:
* - Code blocks (syntax highlighting and preserved whitespace)
* - Columns (table layout instead of flexbox)
* - Equations (inline font instead of KaTeX)
* - Callouts (table layout for icon placement)
* - Headings (bookmark anchors for TOC links)
* - TOC (anchor links with paragraph breaks)
*
* Tables use base version with juice CSS inlining.
*/
export const DocxExportKit = [
BaseCodeBlockPlugin.configure({
component: CodeBlockElementDocx,
}),
BaseColumnItemPlugin.configure({
component: ColumnElementDocx,
}),
BaseColumnPlugin.configure({
component: ColumnGroupElementDocx,
}),
BaseEquationPlugin.configure({
component: EquationElementDocx,
}),
BaseInlineEquationPlugin.configure({
component: InlineEquationElementDocx,
}),
BaseCalloutPlugin.configure({
component: CalloutElementDocx,
}),
BaseHeadingPlugin.configure({
component: HeadingElementDocx,
}),
BaseTocPlugin.configure({
component: TocElementDocx,
}),
];import { BaseHeadingPlugin, BaseCodeBlockPlugin } from 'platejs';
import { BaseCalloutPlugin } from 'platejs/callout';
import { BaseColumnItemPlugin, BaseColumnPlugin } from 'platejs/layout';
import { BaseEquationPlugin, BaseInlineEquationPlugin } from 'platejs/math';
import { BaseTocPlugin } from 'platejs/toc';
import { CalloutElementDocx } from '@/components/editor/callout-static';
import { CodeBlockElementDocx } from '@/components/editor/code-block-static';
import {
ColumnElementDocx,
ColumnGroupElementDocx,
} from '@/components/editor/column-static';
import { HeadingElementDocx } from '@/components/editor/heading-static';
| Content | Behavior |
|---|---|
| Paragraphs, headings, supported marks and properties | Decode through the installed HTML codecs and export through the configured static renderer |
| Lists, links, bookmarks, rows, cells, and spans | Preserve supported semantic structure; omit Word layout details with diagnostics |
| Tracked revisions | Preserve supported insert, delete, move, run-property, and paragraph-property changes |
| Main-body comments | Preserve rich bodies, ranges, author/date metadata, durable IDs, replies, and resolved state when present |
| Embedded images during import | Omit the resource and return a resource-omitted diagnostic |
| Single-section headers and footers | Keep safe closed source subgraphs during source-aware export; do not map them into the editor document |
| Multiple-section headers and footers, footnotes, and endnotes | Inspect and bound their package parts; omit them from edited source-aware output with diagnostics |
| Fields, text boxes, shapes, charts, SmartArt, and embedded objects | Keep reachable plain text when conversion exposes it; report unsupported or omitted content |
| Plate named roots and metadata | Preserve only through an exact matching review envelope; report that the visible Word projection omits them |
| Custom Plate, math, emoji, and media nodes | Use their configured static serializers; unsupported output produces diagnostics |
| Surface | Import path |
|---|---|
WordPastePlugin | platejs/docx/paste |
importDocx and import types | platejs/docx/import |
exportToDocx and export types | platejs/docx/export |
importDocx(
editor: Editor,
source: ArrayBuffer | Blob,
options?: DocxImportOptions
): Promise<DocxImportResult>importDocx(
editor: Editor,
source: ArrayBuffer | Blob,
options?: DocxImportOptions
): Promise<DocxImportResult>DocxImportOptions accepts a cooperative signal, partial limits overrides, and retainSource. Every limit must be a positive safe integer. Literal retainSource: true conditionally adds source to a successful result; omitted or literal false does not.
| Default limit | Value |
|---|---|
maxInputBytes | 32 MiB |
maxEntries | 1,024 |
maxEntryBytes | 32 MiB |
maxExpandedBytes | 128 MiB |
maxRelationships | 4,096 |
maxRevisions | 2,000 |
maxComments | 5,000 |
maxXmlDepth | 128 |
maxXmlNodes | 500,000 |
A successful result contains document, comments, and structured diagnostics. A retained success also contains source. A failed result contains only diagnostics.
class DocxSource {
private constructor();
dispose(): void;
}class DocxSource {
private constructor();
dispose(): void;
}Only importDocx creates a DocxSource. The object is process-local and cannot be serialized or reattached. To restore source-aware export after application reload, keep the original file and import it again.
exportToDocx(
editor: Editor,
options: DocxExportOptions
): Promise<DocxExportResult>exportToDocx(
editor: Editor,
options: DocxExportOptions
): Promise<DocxExportResult>| Option | Type | Description |
|---|---|---|
projection | 'accepted' | 'proposed' | 'review' | Required visible document projection |
comments | readonly DocxComment[] | Comment records in proposed-projection coordinates |
allowRemoteImages | boolean | Fetch remote HTTP images; defaults to false |
editorPlugins | readonly BasePluginInput[] | Descriptors used for static rendering |
editorStaticComponent | React.ComponentType<EditorStaticProps> | Static editor wrapper |
fontFamily | string | Document body font |
margins | Margins | Page margins in twentieths of a point |
orientation | 'landscape' | 'portrait' | Page orientation |
pageSize | PageSize | Page dimensions in twentieths of a point |
signal | AbortSignal | Cooperative cancellation signal |
source | DocxSource | Retained source correspondence for exact or safe export |
stylesheet | string | CSS applied before DOCX conversion |
title | string | Document metadata title |
A successful result contains blob and structured diagnostics. A failed result contains only diagnostics.
See Authored Changes for the editor's accepted, proposed, and review projections.