<EditorStatic> is a fast, read-only React component for rendering Plate content, optimized for server-side or React Server Component (RSC) environments. It avoids client-side editing logic and memoizes node renders for better performance compared to using <EditorRoot> in read-only mode.
It is the rendering path behind renderStaticHtml and fits server or RSC
surfaces that need a non-interactive Plate view.
<EditorStatic>For interactive read-only features (like comment popovers or selections), use the standard <EditorRoot> component in the browser. For purely server-rendered, non-interactive content, <EditorStatic> is the recommended choice.
BaseEditorKit includes base plugins configured for static rendering.
import { BaseUploadPlugin } from 'platejs/upload';
import { AlignKit } from './align';
import { BaseBasicBlocksKit } from './basic-blocks-static';
import { BaseBasicMarksKit } from './basic-marks-static';
import { BaseCalloutKit } from './callout-static';
import { BaseCodeBlockKit } from './code-block-static';
import { BaseCodeDrawingKit } from './code-drawing-static';
import { BaseColumnKit } from './column-static';
import { BaseDateKit } from './date-static';
import { BaseDetailsKit } from './details-static';
import { BaseFontKit } from './font-static';
import { BaseFootnoteKit } from './footnote-static';
import { LineHeightKit } from './line-height';
import { BaseLinkKit } from './link-static';
import { BaseListKit } from './list-static';
import { MarkdownKit } from './markdown';
import { BaseMathKit } from './math-static';
import { BaseMediaKit } from './media-static';
import { BaseMentionKit } from './mention-static';
import { BaseTableKit } from './table-static';
import { BaseTocKit } from './toc-static';
export const BaseEditorKit = [
...BaseBasicBlocksKit,
...BaseCodeBlockKit,
...BaseCodeDrawingKit,
...BaseTableKit,
...BaseDetailsKit,
...BaseTocKit,
...BaseMediaKit,
BaseUploadPlugin,
...BaseCalloutKit,
...BaseColumnKit,
...BaseMathKit,
...BaseDateKit,
...BaseLinkKit,
...BaseMentionKit,
...BaseFootnoteKit,
...BaseBasicMarksKit,
...BaseFontKit,
...BaseListKit,
...AlignKit,
...LineHeightKit,
...MarkdownKit,
] as const;import { BaseUploadPlugin } from 'platejs/upload';
import { AlignKit } from './align';
import { BaseBasicBlocksKit } from './basic-blocks-static';
import { BaseBasicMarksKit } from './basic-marks-static';
import { BaseCalloutKit } from './callout-static';
import { BaseCodeBlockKit } from './code-block-static';
import { BaseCodeDrawingKit } from './code-drawing-static';
import { BaseColumnKit } from './column-static';
import { BaseDateKit } from './date-static';
import { BaseDetailsKit } from './details-static';
import { BaseFontKit } from './font-static'
import { createEditor } from 'platejs';
import { EditorStatic } from 'platejs/static';
import { BaseEditorKit } from '@/components/editor/plugins-static';
const editor = createEditor({
plugins: BaseEditorKit,
initialValue: [
{ type: 'heading', level: 1, children: [{ text: 'Server-Rendered Title' }] },
{ type: 'paragraph', children: [{ text: 'This content is rendered statically.' }] },
],
});
// Render statically
export default function MyStaticPage() {
return <EditorStatic editor={editor} />;
See a complete server-side static rendering example:
import fs from 'node:fs/promises';
import path from 'node:path';
import { cva } from 'class-variance-authority';
import type { Metadata } from 'next';
import type { EditorDocumentValue } from 'platejs';
import { createStaticEditor, renderStaticHtml } from 'platejs/static';
import * as React from 'react';
import { EditorStatic } from '@/components/editor/editor-static';
import {
EditorClient,
EditorViewClient,
ExportHtmlButton,
HtmlIframe,
Initialize a Plate editor instance using createEditor with component-bound plugins. This is analogous to using useCreateEditor for the interactive <EditorRoot> component.
import { createEditor } from 'platejs';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
// Import any other desired base plugins, such as MarkdownPlugin.
// Ensure you are NOT importing from /react subpaths for server environments.
const editor = createEditor({
plugins: [
...BaseBasicBlocksKit,
// Add other base plugins here.
],
initialValue: [
{
type: 'paragraph',
children: [{ text: 'Hello from a static Plate editor!' }],
},
],
});import { createEditor } from 'platejs';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
// Import any other desired base plugins, such as MarkdownPlugin.
// Ensure you are NOT importing from /react subpaths for server environments.
const editor = createEditor({
plugins: [
...BaseBasicBlocksKit,
// Add other base plugins here.
],
initialValue: [
{
type: 'paragraph',
children: [{ text: 'Hello from a static Plate editor!' }],
},
],
});If your interactive editor uses client-side components (e.g., with use client or event handlers), you must create static, server-safe equivalents. These components should render pure HTML without browser-specific logic.
import React from 'react';
import type { BaseParagraphPlugin } from 'platejs';
import { EditorElement, type EditorElementProps } from 'platejs/static';
export function ParagraphElementStatic(
props: EditorElementProps<typeof BaseParagraphPlugin>
) {
return (
<EditorElement {...props}>
{props.children}
</EditorElement>
);
}Create similar static components for headings, images, links, etc.
Configure each server-safe component on its owning base plugin.
import { BaseHeadingPlugin, BaseParagraphPlugin } from 'platejs';
import { ParagraphElementStatic } from './ui/paragraph-static';
import { HeadingElementStatic } from './ui/heading-static';
export const StaticEditorKit = [
BaseParagraphPlugin.configure({ component: ParagraphElementStatic }),
BaseHeadingPlugin.configure({ component: HeadingElementStatic }),
];
<EditorStatic>Use <EditorStatic> with an editor whose plugin descriptors own their static components.
import { createEditor } from 'platejs';
import { EditorStatic } from 'platejs/static';
import { StaticEditorKit } from '@/components/static-editor-kit';
export default async function MyStaticPage() {
// Example: Fetch or define editor value
const initialValue = [
{ type: 'heading', level: 1, children: [{ text: 'Server-Rendered Title' }] },
{ type: 'paragraph', children: [{ text: 'Content rendered statically.' }] },
];
const editor = createEditor({
plugins: StaticEditorKit,
initialValue,
<EditorStatic> enhances performance through memoization:
React.memo.render: { readsDocument: true } on its
plugin and renders again whenever the document changes.Pass document to render a document through the editor's plugins without
editing the editor: no transaction, history or selection change. One configured
editor can render a streamed preview or a stored revision this way:
<EditorStatic editor={editor} document={{ children: blocks }} /><EditorStatic editor={editor} document={{ children: blocks }} />The document must be valid for the editor's schema. The editor validates a
document from editor.read.value() once and any other document object on every
render, because that object can change between renders.
EditorPreview takes the same document, and copying from it copies the
rendered document.
Each render reads the decorations of every top-level block, so rendering again is also how decorations that depend on other state refresh. A component that re-renders for other reasons, such as each streamed chunk, should memoize the element on its document:
const preview = useMemo(
() => <EditorStatic editor={editor} document={document} />,
[editor, document]
);const preview = useMemo(
() => <EditorStatic editor={editor} document={document} />,
[editor, document]
);Everything read through the rendered editor sees the document: editor.read,
plugin reads such as editor.plugin(TocPlugin).read.headings(), and plugin
APIs. Plugins get that editor from their render, decorate, read or API
context. An editor captured when a plugin was created, as in
.extend(({ editor }) => …), is the editor you passed and still holds its own
value. Reading it from plugin code that reads, decorates or renders the
document throws an error that names the fix. React runs component bodies after
that code returns, so a component reads through its editor prop.
For cases where you need minimal interactivity with static content, use <EditorPreview>. This component wraps <EditorStatic> and adds client-side event handlers for user interactions while maintaining the performance benefits of static rendering.
import { createStaticEditor, EditorStatic } from 'platejs/static';
import { BaseEditorKit } from '@/components/editor/plugins-static';
import { InteractiveViewer } from './interactive-viewer';
export default async function DocumentPage() {
const content = await fetchDocument(); // Your document data
// Server-side static editor
const editor = createStaticEditor({
plugins: BaseEditorKit,
initialValue: content,
});
return (
<div className="grid grid-cols-2 gap-4">
{/* Pure static rendering - no interactivity */}
<div>
<h2>Static View (Server Rendered)</h2>
<EditorStatic editor={editor} />
</div>
{/* Interactive view - rendered on client */}
<div>
<h2>Interactive View</h2>
<InteractiveViewer value={content} />
</div>
</div>
);
}import { createStaticEditor, EditorStatic } from 'platejs/static';
import { BaseEditorKit } from '@/components/editor/plugins-static';
import { InteractiveViewer } from './interactive-viewer';
export default async function DocumentPage() {
const content = await fetchDocument(); // Your document data
// Server-side static editor
const editor = createStaticEditor({
plugins: BaseEditorKit,
initialValue: content,
});
return (
<div className="grid grid-cols-2 gap-4">
{/* Pure static rendering - no interactivity */}
<
'use client';
import { EditorPreview, useStaticEditor } from 'platejs/react';
import { BaseEditorKit } from '@/components/editor/plugins-static';
export function InteractiveViewer({ value }) {
const editor = useStaticEditor({
plugins: BaseEditorKit,
initialValue: value,
});
return <EditorPreview editor={editor} />;
}'use client';
import { EditorPreview, useStaticEditor } from 'platejs/react';
import { BaseEditorKit } from '@/components/editor/plugins-static';
export function InteractiveViewer({ value }) {
const editor = useStaticEditor({
plugins: BaseEditorKit,
initialValue: value,
});
return <EditorPreview editor={editor} />;
}'use client' directiveEditorStatic internally for renderinguseStaticEditor: Creates a static editor optimized for view-only React componentsEditorPreview resolves selection against its own
static host and writes the same model slice, HTML, and plain-text payloads as
a mounted editorEditorPreview cannot be used in Server Components. If you're passing an editor from a server component to a client component, you'll encounter serialization errors. Use EditorStatic on the server side, or create the editor client-side with useStaticEditor.
| Aspect | <EditorStatic> | <EditorPreview> | <EditorRoot> + readOnly |
|---|---|---|---|
| Environment | Server/Client (SSR/RSC safe) | Client-only | Client-only |
| Interactivity | None | Minimal (selection, copy, toolbar, etc.) | Full interactive features (browser-only) |
| Browser APIs | Not used | Minimal (event handlers) | Full usage |
| Performance | Best - static HTML only | Good - static rendering + event delegation | Heavier - full editor internals |
| Bundle Size | Smallest | Small | Largest |
| Use Cases | Server rendering, HTML export | Client-side content with basic interactions | Full read-only editor with all features |
| Recommendation | SSR/RSC without any interactions | Client-side content needing light interactivity | Client-side with complex interactive needs |
In a Next.js App Router (or similar RSC environment), <EditorStatic> can be used directly in Server Components:
import { createEditor } from 'platejs';
import { EditorStatic } from 'platejs/static';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
export default async function Page() {
// Fetch or define content server-side
const serverContent = [
{ type: 'heading', level: 1, children: [{ text: 'Rendered on the Server! 🎉' }] },
{ type: 'paragraph', children: [{ text: 'This content is static and server-rendered.' }] },
];
const editor = createEditor({
plugins: [...BaseBasicBlocksKit],
initialValue: serverContent,
});
return (
<EditorStatic
editor={editor}
className="my-static-preview-container"
/>
);
}import { createEditor } from 'platejs';
import { EditorStatic } from 'platejs/static';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
export default async function Page() {
// Fetch or define content server-side
const serverContent = [
{ type: 'heading', level: 1, children: [{ text: 'Rendered on the Server! 🎉' }] },
{ type: 'paragraph', children: [{ text: 'This content is static and server-rendered.' }] },
];
const editor = createEditor({
plugins: [...BaseBasicBlocksKit],
initialValue: serverContent,
});
This renders the content to HTML on the server without needing a client-side JavaScript bundle for EditorStatic itself.
For server-rendering the static Plate tree to an HTML string, use
renderStaticHtml. It renders <EditorStatic> through React DOM Server; it is
not a semantic HTML interchange serializer.
EditorStatic renders configured node components and render attributes. It
omits editing-surface slots such as slots.wrapContent; wrap result.data in
application presentation when the exported document needs an outer shell.
import { createEditor } from 'platejs';
import { renderStaticHtml } from 'platejs/static';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
async function getDocumentAsHtml(value: any[]) {
const editor = createEditor({
plugins: [...BaseBasicBlocksKit],
initialValue: value,
});
const result = await renderStaticHtml(editor, {
// component: EditorStatic, // Optional: Defaults to EditorStatic
props: { className: 'prose max-w-none' }, // Example: Pass props to the root div
});
return result.data;
}
// Example Usage:
// const value = [ { type: 'heading', level: 1, children: [{ text: 'My Document' }] } ];
// getDocumentAsHtml(value).then(console.log);import { createEditor } from 'platejs';
import { renderStaticHtml } from 'platejs/static';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
async function getDocumentAsHtml(value: any[]) {
const editor = createEditor({
plugins: [...BaseBasicBlocksKit],
initialValue: value,
});
const result = await renderStaticHtml(editor, {
// component: EditorStatic, // Optional: Defaults to EditorStatic
props: { className: 'prose max-w-none' }, // Example: Pass props to the root div
});
return result.data;
}
For more details, see the HTML Serialization guide.
import type React from 'react';
import type { Editor, EditorDocumentValue } from 'platejs';
interface EditorStaticProps<E = Editor>
extends React.HTMLAttributes<HTMLDivElement> {
/**
* A document to render instead of the editor's own. The editor supplies
* the plugins and is not edited.
*/
document?: EditorDocumentValue;
/**
* The Plate editor instance, created via `createEditor`.
* Must include component-bound plugins relevant to the rendered content.
*/
editor: E;
/** Inline CSS styles for the root `div` element. */
style?: React.CSSProperties;
// Other HTMLDivElement attributes like `className`, `id`, etc., are also supported.
}import type React from 'react';
import type { Editor, EditorDocumentValue } from 'platejs';
interface EditorStaticProps<E = Editor>
extends React.HTMLAttributes<HTMLDivElement> {
/**
* A document to render instead of the editor's own. The editor supplies
* the plugins and is not edited.
*/
document?: EditorDocumentValue;
/**
* The Plate editor instance, created via `createEditor`.
* Must include component-bound plugins relevant to the rendered content.
*/
editor: E;
/** Inline CSS styles for the root `div` element. */
style?:
document: A document rendered through the editor's plugins instead of the editor's own value; see Render a document.editor: An editor created with createEditor or createStaticEditor, including component-bound plugins required by the value.import { createEditor } from 'platejs';
import { EditorStatic } from 'platejs/static';
import { BaseEditorKit } from '@/components/editor/plugins-static';
const editor = createEditor({
plugins: BaseEditorKit,
initialValue: [
{ type: 'heading', level: 1, children: [{ text: 'Server-Rendered Title' }] },
{ type: 'paragraph', children: [{ text: 'This content is rendered statically.' }] },
],
});
// Render statically
export default function MyStaticPage() {
return <EditorStatic editor={editor} />;
}import fs from 'node:fs/promises';
import path from 'node:path';
import { cva } from 'class-variance-authority';
import type { Metadata } from 'next';
import type { EditorDocumentValue } from 'platejs';
import { createStaticEditor, renderStaticHtml } from 'platejs/static';
import * as React from 'react';
import { EditorStatic } from '@/components/editor/editor-static';
import {
EditorClient,
EditorViewClient,
ExportHtmlButton,
HtmlIframe,
} from '@/components/editor/html-export';
import { HtmlExportKit } from '@/components/editor/html-export-kit';
import { alignValue } from '@/registry/examples/values/align-value';
import { basicBlocksValue } from '@/registry/examples/values/basic-blocks-value';
import { basicMarksValue } from '@/registry/examples/values/basic-marks-value';
import { columnValue } from '@/registry/examples/values/column-value';
import { dateValue } from '@/registry/examples/values/date-value';
import { equationValue } from '@/registry/examples/values/equation-value';
import { fontValue } from '@/registry/examples/values/font-value';
import { indentValue } from '@/registry/examples/values/indent-value';
import { lineHeightValue } from '@/registry/examples/values/line-height-value';
import { linkValue } from '@/registry/examples/values/link-value';
import { listValue } from '@/registry/examples/values/list-value';
import { mediaValue } from '@/registry/examples/values/media-value';
import { mentionValue } from '@/registry/examples/values/mention-value';
import { suggestionValue } from '@/registry/examples/values/suggestion-value';
import { tableValue } from '@/registry/examples/values/table-value';
import { tocPlaygroundValue } from '@/registry/examples/values/toc-value';
export const metadata: Metadata = {
title: 'HTML Export',
};
const getCachedTailwindCss = React.cache(async () => {
const cssPath = path.join(process.cwd(), 'public', 'tailwind.css');
return await fs.readFile(cssPath, 'utf-8');
});
const createHtmlDocument = ({
editorHtml,
katexCDN,
tailwindCss,
theme,
}: {
editorHtml: string;
tailwindCss: string;
katexCDN?: string;
theme?: string;
}) => `<!DOCTYPE html>
<html lang="en"${theme === 'dark' ? ' class="dark"' : ''}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<style>${tailwindCss}</style>
${katexCDN}
<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"
/>
<style>
:root {
--font-sans: 'Inter', 'Inter Fallback';
--font-mono: 'JetBrains Mono', 'JetBrains Mono Fallback';
}
</style>
</head>
<body>
${editorHtml}
</body>
</html>`;
const createValue = (): EditorDocumentValue => ({
children: [
...basicBlocksValue,
...basicMarksValue,
...tocPlaygroundValue,
...linkValue,
...tableValue,
...equationValue,
...columnValue,
...mentionValue,
...dateValue,
...fontValue,
...suggestionValue,
...alignValue,
...lineHeightValue,
...indentValue,
...listValue,
...mediaValue.children,
],
});
export default async function HtmlExportBlock() {
const editor = createStaticEditor({
plugins: HtmlExportKit,
initialValue: createValue(),
});
const tailwindCss = await getCachedTailwindCss();
const katexCDN = `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.18/dist/katex.css" integrity="sha384-9PvLvaiSKCPkFKB1ZsEoTjgnJn+O3KvEwtsz37/XrkYft3DTk2gHdYvd9oWgW3tV" crossorigin="anonymous">`;
// const cookieStore = await cookies();
// const theme = cookieStore.get('theme')?.value;
const theme = 'light';
// Get the editor content HTML using EditorStatic
const { data: editorHtml } = await renderStaticHtml(editor, {
component: EditorStatic,
props: { style: { padding: '0 calc(50% - 350px)', paddingBottom: '' } },
});
// Create the full HTML document
const html = createHtmlDocument({
editorHtml,
katexCDN,
tailwindCss,
theme,
});
return (
<div className="grid grid-cols-3 px-4">
<div className="p-2">
<h3 className={headingVariants()}>Editor</h3>
<EditorClient value={createValue()} />
</div>
<div className="p-2">
<h3 className={headingVariants()}>EditorView</h3>
<EditorViewClient value={createValue()} />
</div>
<div className="relative p-2">
<h3 className={headingVariants()}>HTML Iframe</h3>
<ExportHtmlButton
className="absolute top-10 right-0"
html={html}
serverTheme={theme}
/>
<HtmlIframe
className="h-[7500px] w-full"
html={html}
serverTheme={theme}
/>
</div>
</div>
);
}
const headingVariants = cva(
'group mt-8 scroll-m-20 font-heading font-semibold text-xl tracking-tight'
);import React from 'react';
import type { BaseParagraphPlugin } from 'platejs';
import { EditorElement, type EditorElementProps } from 'platejs/static';
export function ParagraphElementStatic(
props: EditorElementProps<typeof BaseParagraphPlugin>
) {
return (
<EditorElement {...props}>
{props.children}
</EditorElement>
);
}import { createEditor } from 'platejs';
import { EditorStatic } from 'platejs/static';
import { StaticEditorKit } from '@/components/static-editor-kit';
export default async function MyStaticPage() {
// Example: Fetch or define editor value
const initialValue = [
{ type: 'heading', level: 1, children: [{ text: 'Server-Rendered Title' }] },
{ type: 'paragraph', children: [{ text: 'Content rendered statically.' }] },
];
const editor = createEditor({
plugins: StaticEditorKit,
initialValue,
});
return (
<EditorStatic
editor={editor}
style={{ padding: 16 }}
className="my-plate-static-content"
/>
);
}