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 format mappings 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(file, {
plugins: BaseEditorKit,
});
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(file, {
plugins: BaseEditorKit,
});
if (!result.ok) {
return { diagnostics: result.diagnostics };
}
editor.update.value.replace(result.document);
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 then includes source. It is a DocxSource when exact export can return the package unchanged. Otherwise it is null, with a source-unavailable warning that names the first part exact export cannot admit. Keep the source in application state, pass it to exportDocx, and dispose it when the document closes or another file replaces it.
import { exportDocx } from "platejs/docx/export";
import { importDocx } from "platejs/docx/import";
const imported = await importDocx(file, {
plugins: BaseEditorKit,
retainSource: true,
});
if (!imported.ok) return imported.diagnostics;
editor.update.value.replace(imported.document);
const exported = await exportDocx(editor, {
projection: "review",
source: imported.source,
});
imported.source?.dispose();import { exportDocx } from "platejs/docx/export";
import { importDocx } from "platejs/docx/import";
const imported = await importDocx(file, {
plugins: BaseEditorKit,
retainSource: true,
});
if (!imported.ok) return imported.diagnostics;
editor.update.value.replace(imported.document);
const exported = await exportDocx(editor, {
projection: "review",
source: imported.source,
});
imported.source?.dispose();Exact export admits a closed set of passive package content:
word/document.xml, styles, settings, numbering, fonts, and theme;PAGE is the only recognized field instruction. Any other part, relationship, markup namespace, field instruction, or XML processing instruction leaves source null. That includes unsafe or relative hyperlinks, other external relationships, macros, ActiveX controls, OLE objects, imported HTML chunks, external templates, custom XML data, and SVG or EMF images. A null source does not mean the import lost content; exporting without a source generates the document from editor content.
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.
Numbered and bulleted Word paragraphs become list items, and each item joins the list of its Word numbering definition. Word counts every instance of a definition in one sequence per level. A list therefore continues past other paragraphs and across instances, and lists of different definitions that interleave keep their own numbers. A level restarts after a paragraph at the level its lvlRestart names or a shallower one, which by default is the level just above it. An instance's start override resets its level once, at the instance's first paragraph at that level. That item imports with the override as listRestart, and the items after it count on from it, so an inserted item renumbers the rest the way Word does. Each item's listLevels hold its definition's level starts, lvlRestart rules and level text, with its instance's format and level text overrides applied. Word ignores a start or lvlRestart inside a level override, and so does the import. A paragraph whose instance shows its level as bullets imports as a bulleted item and does not count.
exportDocx captures the editor once before asynchronous rendering. Pass the visible projection explicitly.
import { exportDocx } from "platejs/docx/export";
import { createEditor } from "platejs/react";
import { BaseEditorKit } from "@/components/editor/plugins-static";
import { DOCX_EXPORT_STYLES } from "@/components/editor/docx-export";
const editor = createEditor({ plugins: BaseEditorKit });
export async function downloadEditorDocx() {
const result = await exportDocx(editor, {
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 { exportDocx } from "platejs/docx/export";
import { createEditor } from "platejs/react";
import { BaseEditorKit } from "@/components/editor/plugins-static";
import { DOCX_EXPORT_STYLES } from "@/components/editor/docx-export";
const editor = createEditor({ plugins: BaseEditorKit });
export async function downloadEditorDocx() {
const result = await exportDocx(editor, {
orientation: "portrait",
projection: "proposed",
stylesheet: DOCX_EXPORT_STYLES,
title: "Document",
});
if
Choose accepted to omit pending changes, proposed to apply them, or review to write Word tracked-change markup. Accepted and proposed exports warn about omitted pending changes and conflicts resolved to one side. A review export with any conflict returns ok: false and an authored-conflict error because Word revisions cannot represent that state faithfully.
A review file contains what Word displays: text, comments, and tracked revisions with their authors. Importing it restores those revisions as authored changes. Export does not write named roots, document metadata, or exact authored change records, such as their original IDs, to the file. To keep them, save the authored JSON document beside the file, as Serializing describes:
import { parseAuthoredDocument, projectAuthoredReview } from "platejs/authored";
const json = JSON.stringify(projectAuthoredReview(editor.read.value()).review);
const document = parseAuthoredDocument(json);import { parseAuthoredDocument, projectAuthoredReview } from "platejs/authored";
const json = JSON.stringify(projectAuthoredReview(editor.read.value()).review);
const document = parseAuthoredDocument(json);exportDocx rejects content loss by default. When an image cannot be embedded, a supplied comment has no representable range, or a node has no drawing in presentation, the result is ok: false with the loss as an error diagnostic. Set lossPolicy: "allow" to download the file and receive the same loss as warnings:
const result = await exportDocx(editor, {
comments,
lossPolicy: "allow",
projection: "review",
});const result = await exportDocx(editor, {
comments,
lossPolicy: "allow",
projection: "review",
});Named roots, document metadata, removed link destinations, and other properties DOCX cannot represent are warnings under both policies. An accepted export omits a comment whose annotated content that projection excludes, and reports a warning, as Word does when the annotated suggestion is rejected.
Each Plate list becomes one Word list. Its listLevels give the list's level starts, lvlRestart rules and level text. An item with listRestart, or one whose number Word would not reach, gets a numbering instance of its own that overrides that level's start, as Word's Restart Numbering writes it. The items after it count on from it in the list's own instance. Word and a re-import then show the numbers the editor shows, and the restart stays on that item. Word draws one marker style per list level, so an item whose listStyle differs from the first item at its level keeps that first style and reports a list-marker warning under both policies. A Word list level is numbered or unordered, as its first item is, and the exporter writes one set of level rules per list. Bullets and to-dos share unordered levels, and every to-do takes the document's shared to-do numbering. A numbered item at an unordered level, or the reverse, is therefore not part of its list in Word, and an item whose listLevels differ from its list's first item takes that item's rules. A kind split reports a list-structure warning, and so does a numbered or task item whose listLevels differ from its list's first numbered or task item; lossPolicy: 'reject' fails the export. A bullet without listId, as Markdown and AI output write it, stays out of the kind check, because a Markdown bullet list never shares a list with a numbered one. Word has no start that applies only when an item opens its level, and it takes each level's start from the list's first item, 1 by default. A numbered or task item whose listStart differs from the start its list's first numbered or task item gives that level exports its shown number and reports a list-start warning under both policies. Bullets take no part in the level-rule and start checks, so a bullet that leads its list gives Word its level rules and starts without a warning.
A heading or blockquote that is a list item keeps its heading style and bookmark or its quote border, and takes its number and indent from its list level. A table cell exports its blocks as the body does, and a nested table and its images fit the cell that holds them.
Export writes a link destination only when it is absolute and meets the navigation floor, such as https: or mailto:. It writes bookmark links such as #introduction as Word anchors. An unsafe destination, or a relative link that has no document base in a Word file, keeps its text without the destination.
Images are embedded as PNG, JPEG, GIF, or BMP. Set allowRemoteImages: true to fetch HTTP(S) images during export. These images are omitted and leave their alt text:
allowRemoteImages is off;Before returning a generated file, export checks the written package against the same passive vocabulary that retained sources use. Output outside it is withheld with an invalid-package error. An unchanged source-aware export returns the retained file, which passed the same check when it was imported.
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, pageSize, stylesheet, or fontFamily option. component and presentation do not apply on this path, because export regenerates nothing.
If an exact check fails, exportDocx regenerates the complete document body. It keeps these source units when their internal relationships are closed, reachable, and nonconflicting:
The regenerated package has its own core document properties. It omits source body XML, body-only parts such as footnotes and body images, and headers and footers from a document with multiple sections. It also omits the source core and extended properties and the package thumbnail: the core properties, such as the author, title and creation date, the extended properties, which count pages and words and hold details such as the company name, and the thumbnail, which pictures the body. Diagnostics report why the source was rewritten and identify each omitted source part. A custom property linked to body text keeps the value cached in the source file, even when the edit changed that text.
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 exportDocx(editor, {
comments,
projection: "review",
});const result = await exportDocx(editor, {
comments,
projection: "review",
});The same configured static renderer handles the main document and rich comment bodies. Under the default lossPolicy, a comment without a complete range fails the export; with lossPolicy: "allow" the export omits it with a warning.
DOCX owns its semantic static mappings for code blocks, columns, equations,
callouts, headings, and tables of contents. They set no color, font, or font
size beyond an element's own values, such as a callout's color. The app's
stylesheet sets them and can select:
editor-<plugin name>, the class on each drawn element, such as .editor-codeBlock;table > tbody > tr > td inside a callout, whose first cell holds the icon;[data-docx-preserve-whitespace], the text of a code block;[data-empty], the placeholder of an empty equation or table of contents.The app may pass a custom root component when it needs a different wrapper.
DOCX_EXPORT_STYLES is the registry's copied presentation preset.
Equations export as Word math built from KaTeX's MathML. An equation that uses
notation the converter does not cover, such as a matrix, exports its TeX as
text inside a Word equation. A task list item, an li with data-checked, exports with a ☑ or ☐
list marker, and import reads a bullet level drawn as ☐, ☑ or ☒ as a task item with
its checked state.
A live editor's components are editing UI. Pass the app's static kit as
presentation so the body and every comment body draw with static components
of the same plugin name. A plugin with no static version exports its content
without its component or wrapper and reports a missing-static-presentation
diagnostic, which counts as lost content under lossPolicy.
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; }
.editor-callout > table > tbody > tr > td { background-color: #f4f4f5; }
.editor-callout > table > tbody > tr > td:first-child {
font-family: "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols;
font-size: 18px;
}
.editor-codeBlock > [data-docx-preserve-whitespace] {
background-color: #f5f5f5;
font-family: 'Courier New', Consolas, monospace;
font-size: 10pt;
}
.editor-equation [data-empty],
.editor-inlineEquation [data-empty] { color: #888; }
.editor-toc [data-empty] { color: #666; font-size: 10pt; }
`.trim();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;
| Content | Behavior |
|---|---|
| Paragraphs, headings, supported marks and properties | Decode through the installed HTML mappings 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 |
| Links and images during export | Write absolute navigation-safe links and PNG, JPEG, GIF, or BMP images; report removed destinations and omitted images |
| 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. Import reports them as dropped content, except a footnote or endnote part that holds only Word's separator notes |
| 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 | Omit from Word output with a lossy-content warning; keep them in authored JSON |
| Equations | Export as Word math, or as TeX inside a Word equation when the converter does not cover the notation. Import reads Word math into equation and inlineEquation nodes, with TeX rebuilt from the equation's structure, or into TeX text when those plugins are not installed. A display equation that shares its paragraph with a comment, tracked change or text, or sits in a list item or an indented paragraph, imports inline |
| Task lists | Give an item drawn as an li with data-checked a ☑ or ☐ list marker; import a bullet level drawn as ☐, ☑ or ☒ as a task item with its checked state |
| Custom Plate, 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 |
exportDocx and export types | platejs/docx/export |
importDocx(
source: ArrayBuffer | Blob,
options: DocxImportOptions
): Promise<DocxImportResult>importDocx(
source: ArrayBuffer | Blob,
options: DocxImportOptions
): Promise<DocxImportResult>DocxImportOptions requires plugins and accepts schema, a cooperative
signal, lossPolicy, partial limits overrides, and
retainSource. Import captures that detached target before its first await and
never reads a live editor or mutable plugin store. Every limit must be a
positive safe integer. Literal retainSource: true conditionally adds
source: DocxSource | null 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, which is null when exact export cannot admit the package. 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.
exportDocx(
editor: Editor,
options: DocxExportOptions
): Promise<DocxExportResult>exportDocx(
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(S) images to embed; defaults to false |
component | React.ComponentType<EditorStaticProps> | Optional static editor wrapper |
fontFamily | string | Document body font; rebuilds an unchanged source |
lossPolicy | 'allow' | 'reject' | Fail on dropped content, or return it as warnings; defaults to 'reject' |
margins | Margins | Page margins in twentieths of a point |
orientation | 'landscape' | 'portrait' | Page orientation |
pageSize | PageSize | Page dimensions in twentieths of a point |
presentation | readonly BasePluginInput[] | Static plugins that draw the body and comment bodies, matched by plugin name |
signal | AbortSignal | Cooperative cancellation signal |
source | DocxSource | null | Retained source correspondence for exact or safe export |
stylesheet | string | CSS that sets the Word look; rebuilds an unchanged source |
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.