Install the Export Toolbar Button component to add the menu to an editor toolbar.
'use client';
import { exportToDocx } from '@platejs/docx-export';
import { MarkdownPlugin } from '@platejs/markdown';
import { ArrowDownToLineIcon } from 'lucide-react';
import { createBaseEditor } from 'platejs';
import { useEditor } from 'platejs/react';
import { renderStaticHtml } from 'platejs/static';
import * as React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { DocxExportKit } from '@/components/editor/docx-export';
import { BaseEditorKit } from '@/components/editor/plugins-static';
import { ToolbarButton } from '@/components/editor/toolbar';
import { EditorStatic } from './editor-static';
const siteUrl = 'https://platejs.org';
const downloadFile = async (url: string, filename: string) => {
const response = await fetch(url);
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = filename;
document.body.append(link);
link.click();
link.remove();
window.URL.revokeObjectURL(blobUrl);
};
export function ExportToolbarButton() {
const editor = useEditor();
const [open, setOpen] = React.useState(false);
const getCanvas = async () => {
const { default: html2canvas } = await import('html2canvas-pro');
const style = document.createElement('style');
document.head.append(style);
const editorElement = editor.api.dom.resolveDOMNode(editor);
if (!editorElement) {
style.remove();
throw new Error('Cannot resolve editor DOM node for export.');
}
const canvas = await html2canvas(editorElement, {
onclone: (document: Document) => {
const innerEditorElement = document.querySelector(
'[contenteditable="true"]'
);
if (innerEditorElement) {
Array.from(innerEditorElement.querySelectorAll('*')).forEach(
(element) => {
const existingStyle = element.getAttribute('style') || '';
element.setAttribute(
'style',
`${existingStyle}; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important`
);
}
);
}
},
});
style.remove();
return canvas;
};
const exportToPdf = async () => {
const [canvas, PDFLib] = await Promise.all([
getCanvas(),
import('pdf-lib'),
]);
const pdfDoc = await PDFLib.PDFDocument.create();
const page = pdfDoc.addPage([canvas.width, canvas.height]);
const imageEmbed = await pdfDoc.embedPng(canvas.toDataURL('PNG'));
const { height, width } = imageEmbed.scale(1);
page.drawImage(imageEmbed, {
height,
width,
x: 0,
y: 0,
});
const pdfBase64 = await pdfDoc.saveAsBase64({ dataUri: true });
await downloadFile(pdfBase64, 'plate.pdf');
};
const exportToImage = async () => {
const canvas = await getCanvas();
await downloadFile(canvas.toDataURL('image/png'), 'plate.png');
};
const exportToHtml = async () => {
const editorStatic = createBaseEditor({
plugins: BaseEditorKit,
initialValue: editor.read.children(),
});
const editorHtml = await renderStaticHtml(editorStatic, {
editorComponent: EditorStatic,
props: { style: { padding: '0 calc(50% - 350px)', paddingBottom: '' } },
});
const tailwindCss = `<link rel="stylesheet" href="${siteUrl}/tailwind.css">`;
const katexCss = `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.18/dist/katex.css" integrity="sha384-9PvLvaiSKCPkFKB1ZsEoTjgnJn+O3KvEwtsz37/XrkYft3DTk2gHdYvd9oWgW3tV" crossorigin="anonymous">`;
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&family=JetBrains+Mono:wght@400..700&display=swap"
rel="stylesheet"
/>
${tailwindCss}
${katexCss}
<style>
:root {
--font-sans: 'Inter', 'Inter Fallback';
--font-mono: 'JetBrains Mono', 'JetBrains Mono Fallback';
}
</style>
</head>
<body>
${editorHtml}
</body>
</html>`;
const url = `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
await downloadFile(url, 'plate.html');
};
const exportToMarkdown = async () => {
const md = editor.plugin(MarkdownPlugin).api.serialize();
const url = `data:text/markdown;charset=utf-8,${encodeURIComponent(md)}`;
await downloadFile(url, 'plate.md');
};
const exportToWord = async () => {
const blob = await exportToDocx(editor.read.value().children, {
editorPlugins: [...BaseEditorKit, ...DocxExportKit],
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'plate.docx';
document.body.append(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
};
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger asChild>
<ToolbarButton pressed={open} tooltip="Export" isDropdown>
<ArrowDownToLineIcon className="size-4" />
</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuGroup>
<DropdownMenuItem onSelect={exportToHtml}>
Export as HTML
</DropdownMenuItem>
<DropdownMenuItem onSelect={exportToPdf}>
Export as PDF
</DropdownMenuItem>
<DropdownMenuItem onSelect={exportToImage}>
Export as Image
</DropdownMenuItem>
<DropdownMenuItem onSelect={exportToMarkdown}>
Export as Markdown
</DropdownMenuItem>
<DropdownMenuItem onSelect={exportToWord}>
Export as Word
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
}'use client';
import { exportToDocx } from '@platejs/docx-export';
import { MarkdownPlugin } from '@platejs/markdown';
import { ArrowDownToLineIcon } from 'lucide-react';
import { createBaseEditor } from 'platejs';
import { useEditor } from 'platejs/react';
import { renderStaticHtml } from 'platejs/static';
import * as React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
| Format | Implementation | Output |
|---|---|---|
| HTML | renderStaticHtml from platejs/static with BaseEditorKit and EditorStatic | plate.html |
html2canvas-pro snapshot converted with pdf-lib | plate.pdf | |
| Image | html2canvas-pro snapshot | plate.png |
| Markdown | editor.api.markdown.serialize() from MarkdownPlugin | plate.md |
| Word | exportToDocx(editor.read.children(), { editorPlugins }) from @platejs/docx-export | plate.docx |
The PDF and image exporters snapshot the current editable DOM. Use them for a quick client-side download, not for paginated print layout.
Word export passes the base static editor kit plus DocxExportKit so custom
nodes can render with DOCX-friendly components.
import { BaseHeadingPlugin } from '@platejs/basic-nodes';
import { BaseCalloutPlugin } from '@platejs/callout';
import {
BaseCodeBlockPlugin,
BaseCodeHighlightPlugin,
BaseCodeLinePlugin,
} from '@platejs/code-block';
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,
CodeLineElementDocx,
CodeSyntaxLeafDocx,
} 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';
/**
* 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, line breaks)
* - 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,
}),
BaseCodeLinePlugin.configure({
component: CodeLineElementDocx,
}),
BaseCodeHighlightPlugin.configure({
component: CodeSyntaxLeafDocx,
}),
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 } from '@platejs/basic-nodes';
import { BaseCalloutPlugin } from '@platejs/callout';
import {
BaseCodeBlockPlugin,
BaseCodeHighlightPlugin,
BaseCodeLinePlugin,
} from '@platejs/code-block';
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,
CodeLineElementDocx,
CodeSyntaxLeafDocx,
} from '@/components/editor/code-block-static'
DocxExportKit overrides code blocks, columns, equations, callouts, and table
of contents rendering for the DOCX conversion path.
Plate Plus includes a server-side export flow for PDF output with page settings.
@platejs/docx-export options and plugin APIs.