This example exports the current Plate editor value from the browser. The
registry export-toolbar-button owns the client-side download menu for HTML,
PDF, image, Markdown, and Word output.
Demo
Loading…
Toolbar Source
Install the Export Toolbar Button component to add the menu to an editor toolbar.
'use client';
import * as React from 'react';
import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
import { exportToDocx } from '@platejs/docx-io';
import { MarkdownPlugin } from '@platejs/markdown';
import { ArrowDownToLineIcon } from 'lucide-react';
import type { SlatePlugin } from 'platejs';
import { createSlateEditor } from 'platejs';
import { useEditorRef } from 'platejs/react';
import { serializeHtml } from 'platejs/static';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { BaseEditorKit } from '@/components/editor/editor-base-kit';
import { EditorStatic } from './editor-static';
import { ToolbarButton } from './toolbar';
import { DocxExportKit } from '@/components/editor/plugins/docx-export-kit';
const siteUrl = 'https://platejs.org';
export function ExportToolbarButton(props: DropdownMenuProps) {
const editor = useEditorRef();
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 canvas = await html2canvas(editor.api.toDOMNode(editor)!, {
onclone: (document: Document) => {
const editorElement = document.querySelector(
'[contenteditable="true"]'
);
if (editorElement) {
Array.from(editorElement.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 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();
// Clean up the blob URL
window.URL.revokeObjectURL(blobUrl);
};
const exportToPdf = async () => {
const canvas = await getCanvas();
const PDFLib = await 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 = createSlateEditor({
plugins: BaseEditorKit,
value: editor.children,
});
const editorHtml = await serializeHtml(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/[email protected]/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:[email protected]&family=JetBrains+Mono:[email protected]&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.getApi(MarkdownPlugin).markdown.serialize();
const url = `data:text/markdown;charset=utf-8,${encodeURIComponent(md)}`;
await downloadFile(url, 'plate.md');
};
const exportToWord = async () => {
const blob = await exportToDocx(editor.children, {
editorPlugins: [...BaseEditorKit, ...DocxExportKit] as SlatePlugin[],
});
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} {...props}>
<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 * as React from 'react';
import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu';
import { exportToDocx } from '@platejs/docx-io';
import { MarkdownPlugin } from '@platejs/markdown';
import { ArrowDownToLineIcon } from 'lucide-react';
import type { SlatePlugin } from 'platejs';
import { createSlateEditor } from 'platejs';
import { useEditorRef } from 'platejs/react';
import { serializeHtml } from 'platejs/static';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { BaseEditorKit } from '@/components/editor/editor-base-kit';
import { EditorStatic } from './editor-static';
import { ToolbarButton } from './toolbar';
import { DocxExportKit } from '@/components/editor/plugins/docx-export-kit';
const siteUrl = 'https://platejs.org';
export function ExportToolbarButton(props: DropdownMenuProps) {
const editor = useEditorRef();
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 canvas = await html2canvas(editor.api.toDOMNode(editor)!, {
onclone: (document: Document) => {
const editorElement = document.querySelector(
'[contenteditable="true"]'
);
if (editorElement) {
Array.from(editorElement.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 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();
// Clean up the blob URL
window.URL.revokeObjectURL(blobUrl);
};
const exportToPdf = async () => {
const canvas = await getCanvas();
const PDFLib = await 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 = createSlateEditor({
plugins: BaseEditorKit,
value: editor.children,
});
const editorHtml = await serializeHtml(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/[email protected]/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:[email protected]&family=JetBrains+Mono:[email protected]&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.getApi(MarkdownPlugin).markdown.serialize();
const url = `data:text/markdown;charset=utf-8,${encodeURIComponent(md)}`;
await downloadFile(url, 'plate.md');
};
const exportToWord = async () => {
const blob = await exportToDocx(editor.children, {
editorPlugins: [...BaseEditorKit, ...DocxExportKit] as SlatePlugin[],
});
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} {...props}>
<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>
);
}Export Formats
| Format | Implementation | Output |
|---|---|---|
| HTML | serializeHtml 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.getApi(MarkdownPlugin).markdown.serialize() | plate.md |
| Word | exportToDocx(editor.children, { editorPlugins }) from @platejs/docx-io | 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.
DOCX Static Kit
Word export passes the base static editor kit plus DocxExportKit so custom
nodes can render with DOCX-friendly components.
/**
* Editor kit optimized for DOCX export.
*
* Uses docx-specific static components for elements that require
* inline styles instead of Tailwind classes (which don't work in DOCX):
* - Code blocks: Need inline syntax highlighting colors and line breaks
* - Columns: Need table layout instead of flexbox
* - Equations: Need inline font styling (KaTeX doesn't work in DOCX)
* - Callouts: Need table layout for icon + content
* - TOC: Need anchor links with proper paragraph breaks
*
* Tables use base version with juice CSS inlining.
*/
import { CalloutElementDocx } from '@/components/ui/callout-node-static';
import {
CodeBlockElementDocx,
CodeLineElementDocx,
CodeSyntaxLeafDocx,
} from '@/components/ui/code-block-node-static';
import {
ColumnElementDocx,
ColumnGroupElementDocx,
} from '@/components/ui/column-node-static';
import {
EquationElementDocx,
InlineEquationElementDocx,
} from '@/components/ui/equation-node-static';
import { TocElementDocx } from '@/components/ui/toc-node-static';
import { DocxExportPlugin } from '@platejs/docx-io';
import { KEYS } from 'platejs';
/**
* 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)
* - TOC (anchor links with paragraph breaks)
*
* Tables use base version with juice CSS inlining.
*/
export const DocxExportKit = [
DocxExportPlugin.configure({
override: {
components: {
[KEYS.codeBlock]: CodeBlockElementDocx,
[KEYS.codeLine]: CodeLineElementDocx,
[KEYS.codeSyntax]: CodeSyntaxLeafDocx,
[KEYS.column]: ColumnElementDocx,
[KEYS.columnGroup]: ColumnGroupElementDocx,
[KEYS.equation]: EquationElementDocx,
[KEYS.inlineEquation]: InlineEquationElementDocx,
[KEYS.callout]: CalloutElementDocx,
[KEYS.toc]: TocElementDocx,
},
},
}),
];/**
* Editor kit optimized for DOCX export.
*
* Uses docx-specific static components for elements that require
* inline styles instead of Tailwind classes (which don't work in DOCX):
* - Code blocks: Need inline syntax highlighting colors and line breaks
* - Columns: Need table layout instead of flexbox
* - Equations: Need inline font styling (KaTeX doesn't work in DOCX)
* - Callouts: Need table layout for icon + content
* - TOC: Need anchor links with proper paragraph breaks
*
* Tables use base version with juice CSS inlining.
*/
import { CalloutElementDocx } from '@/components/ui/callout-node-static';
import {
CodeBlockElementDocx,
CodeLineElementDocx,
CodeSyntaxLeafDocx,
} from '@/components/ui/code-block-node-static';
import {
ColumnElementDocx,
ColumnGroupElementDocx,
} from '@/components/ui/column-node-static';
import {
EquationElementDocx,
InlineEquationElementDocx,
} from '@/components/ui/equation-node-static';
import { TocElementDocx } from '@/components/ui/toc-node-static';
import { DocxExportPlugin } from '@platejs/docx-io';
import { KEYS } from 'platejs';
/**
* 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)
* - TOC (anchor links with paragraph breaks)
*
* Tables use base version with juice CSS inlining.
*/
export const DocxExportKit = [
DocxExportPlugin.configure({
override: {
components: {
[KEYS.codeBlock]: CodeBlockElementDocx,
[KEYS.codeLine]: CodeLineElementDocx,
[KEYS.codeSyntax]: CodeSyntaxLeafDocx,
[KEYS.column]: ColumnElementDocx,
[KEYS.columnGroup]: ColumnGroupElementDocx,
[KEYS.equation]: EquationElementDocx,
[KEYS.inlineEquation]: InlineEquationElementDocx,
[KEYS.callout]: CalloutElementDocx,
[KEYS.toc]: TocElementDocx,
},
},
}),
];DocxExportKit overrides code blocks, columns, equations, callouts, and table
of contents rendering for the DOCX conversion path.
Plus Export
Plate Plus includes a server-side export flow for PDF output with page settings.
Related
- DOCX Import/Export covers
@platejs/docx-iooptions and plugin APIs. - Markdown covers Markdown serialization.
- Static Rendering covers static editor rendering and HTML serialization.