Convert Plate content to HTML and vice-versa.

This guide covers converting Plate editor content to HTML (serializeHtml) and parsing HTML back into Plate's format (editor.api.html.deserialize).

Loading...
Files
components/demo.tsx
'use client';

import * as React from 'react';

import { Plate, usePlateEditor } from 'platejs/react';

import { EditorKit } from '@/components/editor/editor-kit';
import { Editor, EditorContainer } from '@/components/ui/editor';

import { DEMO_VALUES } from './values/demo-values';

export default function Demo({ id }: { id: string }) {
  const editor = usePlateEditor({
    plugins: EditorKit,
    value: DEMO_VALUES[id],
  });

  return (
    <Plate editor={editor}>
      <EditorContainer variant="demo">
        <Editor />
      </EditorContainer>
    </Plate>
  );
}

Kit Usage

Installation

The fastest way to enable HTML serialization is with the BaseEditorKit, which includes pre-configured base plugins that support HTML conversion for most common elements and marks.

import { BaseAlignKit } from './plugins/align-base-kit';
import { BaseBasicBlocksKit } from './plugins/basic-blocks-base-kit';
import { BaseBasicMarksKit } from './plugins/basic-marks-base-kit';
import { BaseCalloutKit } from './plugins/callout-base-kit';
import { BaseCodeBlockKit } from './plugins/code-block-base-kit';
import { BaseColumnKit } from './plugins/column-base-kit';
import { BaseCommentKit } from './plugins/comment-base-kit';
import { BaseDateKit } from './plugins/date-base-kit';
import { BaseFontKit } from './plugins/font-base-kit';
import { BaseLineHeightKit } from './plugins/line-height-base-kit';
import { BaseLinkKit } from './plugins/link-base-kit';
import { BaseListKit } from './plugins/list-base-kit';
import { MarkdownKit } from './plugins/markdown-kit';
import { BaseMathKit } from './plugins/math-base-kit';
import { BaseMediaKit } from './plugins/media-base-kit';
import { BaseMentionKit } from './plugins/mention-base-kit';
import { BaseSuggestionKit } from './plugins/suggestion-base-kit';
import { BaseTableKit } from './plugins/table-base-kit';
import { BaseTocKit } from './plugins/toc-base-kit';
import { BaseToggleKit } from './plugins/toggle-base-kit';
 
export const BaseEditorKit = [
  ...BaseBasicBlocksKit,
  ...BaseCodeBlockKit,
  ...BaseTableKit,
  ...BaseToggleKit,
  ...BaseTocKit,
  ...BaseMediaKit,
  ...BaseCalloutKit,
  ...BaseColumnKit,
  ...BaseMathKit,
  ...BaseDateKit,
  ...BaseLinkKit,
  ...BaseMentionKit,
  ...BaseBasicMarksKit,
  ...BaseFontKit,
  ...BaseListKit,
  ...BaseAlignKit,
  ...BaseLineHeightKit,
  ...BaseCommentKit,
  ...BaseSuggestionKit,
  ...MarkdownKit,
];

Add Kit

import { createSlateEditor, serializeHtml } from 'platejs';
import { BaseEditorKit } from '@/components/editor/editor-base-kit';
 
const editor = createSlateEditor({
  plugins: BaseEditorKit,
  value: [
    { type: 'h1', children: [{ text: 'Hello World' }] },
    { type: 'p', children: [{ text: 'This content will be serialized to HTML.' }] },
  ],
});
 
// Serialize to HTML
const html = await serializeHtml(editor);

Example

See a complete server-side HTML generation example:

import * as React from 'react';
 
import { cva } from 'class-variance-authority';
import fs from 'node:fs/promises';
import path from 'node:path';
import { type Value, createSlateEditor, serializeHtml } from 'platejs';
 
import { BaseEditorKit } from '@/components/editor/editor-base-kit';
import {
  EditorClient,
  ExportHtmlButton,
  HtmlIframe,
} from '@/components/editor/slate-to-html';
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 { discussionValue } from '@/registry/examples/values/discussion-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 { tableValue } from '@/registry/examples/values/table-value';
import { tocPlaygroundValue } from '@/registry/examples/values/toc-value';
import { createHtmlDocument } from '@/lib/create-html-document';
import { EditorStatic } from '@/components/ui/editor-static';
 
const getCachedTailwindCss = React.cache(async () => {
  const cssPath = path.join(process.cwd(), 'public', 'tailwind.css');
 
  return await fs.readFile(cssPath, 'utf8');
});
 
export default async function SlateToHtmlBlock() {
  const createValue = (): Value => [
    ...basicBlocksValue,
    ...basicMarksValue,
    ...tocPlaygroundValue,
    ...linkValue,
    ...tableValue,
    ...equationValue,
    ...columnValue,
    ...mentionValue,
    ...dateValue,
    ...fontValue,
    ...discussionValue,
    ...alignValue,
    ...lineHeightValue,
    ...indentValue,
    ...listValue,
    ...mediaValue,
  ];
 
  const editor = createSlateEditor({
    plugins: BaseEditorKit,
    value: createValue(),
  });
 
  const tailwindCss = await getCachedTailwindCss();
  const katexCDN = `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/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 editorHtml = await serializeHtml(editor, {
    editorComponent: 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()}>EditorStatic</h3>
        <EditorStatic editor={editor} />
      </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 text-xl font-semibold tracking-tight'
);

Plate to HTML

Convert Plate editor content (Plate nodes) into an HTML string. This is often done server-side.

View Server-Side Example

Key Server-Side Constraint

When using serializeHtml or other Plate utilities in a server environment (Node.js, RSC), you must not import from /react subpaths of any platejs* package. Always use the base imports (e.g., @platejs/basic-nodes instead of @platejs/basic-nodes/react).

This means you should use createSlateEditor from platejs for server-side editor instances, not usePlateEditor or createPlateEditor from platejs/react.

Basic Usage

Provide a server-side editor instance and configure your Plate components during editor creation.

lib/generate-html.ts
import { createSlateEditor, serializeHtml } from 'platejs'; // Base import
// Import base plugins (NOT from /react paths)
import { BaseHeadingPlugin } from '@platejs/basic-nodes';
// Import your STATIC components for rendering
import { ParagraphElementStatic } from '@/components/ui/paragraph-node-static';
import { HeadingElementStatic } from '@/components/ui/heading-node-static';
// For a styled static output, you might use a wrapper like EditorStatic
import { EditorStatic } from '@/components/ui/editor-static';
 
// Map plugin keys to their STATIC rendering components
const components = {
  p: ParagraphElementStatic, // 'p' is the default key for paragraphs
  h1: HeadingElementStatic,
  // ... add mappings for all your elements and marks
};
 
// Create a server-side editor instance with components
const editor = createSlateEditor({
  plugins: [
    BaseHeadingPlugin,   // Base plugin for headings
    // ... add all other base plugins relevant to your content
  ],
  components,
});
 
async function getMyHtml() {
  // Example: set some content on the server-side editor
  editor.children = [
    { type: 'h1', children: [{text: 'My Title'}] },
    { type: 'p', children: [{text: 'My content.'}] }
  ];
 
  const html = await serializeHtml(editor, {
    // Optional: Use a custom wrapper like EditorStatic for styling
    // editorComponent: EditorStatic,
    // props: { variant: 'none', className: 'p-4 m-4 border' },
  });
 
  return html;
}

Styling Serialized HTML

serializeHtml returns only the HTML for the editor content itself. If you use styled components (like EditorStatic or custom static components with specific classes), you must ensure the necessary CSS is available in the final context where the HTML will be displayed.

This often means wrapping the serialized HTML in a full HTML document that includes your stylesheets:

lib/generate-full-html-document.ts
// ... (previous setup from generate-html.ts)
 
async function getFullHtmlDocument() {
  const editorHtmlContent = await getMyHtml(); // From previous example
 
  const fullHtml = `<!DOCTYPE html>
  <html>
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <link rel="stylesheet" href="/path/to/your-global-styles.css" />
      <link rel="stylesheet" href="/path/to/tailwind-or-component-styles.css" />
      <title>Serialized Content</title>
    </head>
    <body>
      <div class="my-document-wrapper prose dark:prose-invert">
        ${editorHtmlContent}
      </div>
    </body>
  </html>`;
  return fullHtml;
}

Static Output Only

The serialization process converts Plate nodes to static HTML. Interactive features (React event handlers, client-side hooks) or components relying on browser APIs will not function in the serialized output.

Using Static Components

For server-side serialization, you must use static versions of your components (no client-only code, no React hooks like useEffect or useState).

Refer to the Static Rendering Guide for detailed instructions on creating server-safe static components for your Plate elements and marks.

components/ui/paragraph-node-static.tsx
import React from 'react';
import type { SlateElementProps } from 'platejs';
 
// Example static paragraph component
export function ParagraphElementStatic(props: SlateElementProps) {
  return (
    <SlateElement {...props} className={cn('m-0 px-0 py-1')}>
      {props.children}
    </SlateElement>
  );
}

HTML to Plate

The HTML deserializer allows you to convert HTML content (strings or DOM elements) back into Plate format. This supports round-trip conversion, preserving structure, formatting, and attributes where corresponding plugin rules exist.

Basic Usage

Use editor.api.html.deserialize within a client-side Plate editor context.

components/my-html-importer.tsx
import { PlateEditor, usePlateEditor } from 'platejs/react'; // React-specific imports for client-side
// Import ALL Plate plugins needed to represent the HTML content
import { HeadingPlugin } from '@platejs/basic-nodes/react';
// ... and so on for bold, italic, tables, lists, etc.
 
function MyHtmlImporter({ htmlString }: { htmlString: string }) {
  const editor = usePlateEditor({
    plugins: [
      HeadingPlugin,     // For <h1>, <h2>, etc.
      // ... include all plugins corresponding to the HTML you expect to parse
    ],
  });
 
  const handleImport = () => {
    const slateValue = editor.api.html.deserialize(htmlString);
    editor.tf.setValue(slateValue);
  };
 
  // ... render your editor and a button to trigger handleImport ...
  return <button onClick={handleImport}>Import HTML</button>;
}

Client-Side Operation

HTML deserialization using editor.api.html.deserialize is typically a client-side operation as it interacts with a live Plate editor instance configured with React components and plugins.

Plugin Deserialization Rules Overview

Each Plate plugin can define rules for how it interprets specific HTML tags, styles, and attributes during deserialization. Below is a summary of common HTML structures and the Plate plugins typically responsible for them.

HTML Element / StylePlate Plugin (Typical)Notes
<strong>, <b>, font-weight: 600,700,boldBoldPluginConverts to bold: true mark.
<em>, <i>, font-style: italicItalicPluginConverts to italic: true mark.
<u>, text-decoration: underlineUnderlinePluginConverts to underline: true mark.
<s>, <del>, <strike>, text-decoration: line-throughStrikethroughPluginConverts to strikethrough: true mark.
<sub>, vertical-align: subSubscriptPluginConverts to subscript: true mark.
<sup>, vertical-align: superSuperscriptPluginConverts to superscript: true mark.
<code> (not in <pre>), font-family: ConsolasCodePluginConverts to code: true mark (inline code).
<kbd>KbdPluginConverts to kbd: true mark.
<p>ParagraphPluginConverts to paragraph element.
<h1> - <h6>HeadingPluginConverts to corresponding heading elements (h1 - h6).
<ul>ListPlugin (classic)Converts to unordered list (ul type). Items become li.
<ol>ListPlugin (classic)Converts to ordered list (ol type). Items become li.
<li> (within <ul> or <ol>)ListPlugin (classic)Converts to list item (li type), with lic (list item content) child.
<li> (with aria-level for indent)ListPluginConverts to paragraph with indent and listStyleType props.
<blockquote>BlockquotePluginConverts to blockquote element.
<pre> (often with <code> inside)CodeBlockPluginConverts to code_block element. Content split into code_line.
<hr>HorizontalRulePluginConverts to horizontal rule element.
<a>LinkPluginConverts to link element (a type) with url property.
<img>ImagePluginConverts to image element (img type) with url property.
<iframe>MediaEmbedPluginConverts to media embed element, attempting to parse URL.
<table>TablePluginConverts to table element.
<tr>TablePluginConverts to tr (table row) element.
<td>TablePluginConverts to td (table cell) element.
<th>TablePluginConverts to th (table header cell) element.
style="background-color: ..."FontColorPluginConverts to backgroundColor mark. (Plugin name might seem inverse)
style="color: ..."FontColorPluginConverts to color mark.
style="font-family: ..."FontFamilyPluginConverts to fontFamily mark.
style="font-size: ..."FontSizePluginConverts to fontSize mark.
style="font-weight: ..." (other than bold values)FontWeightPluginConverts to fontWeight mark for non-standard bold values.
<mark>HighlightPluginConverts to highlight: true mark.
style="text-align: ..."TextAlignPluginSets align property on block elements.
style="line-height: ..."LineHeightPluginSets lineHeight property on block elements.

Plugin Configuration

The exact Plate type (e.g., ParagraphPlugin.key vs. 'p') depends on how plugins are configured. The table shows typical associations. Ensure the corresponding Plate plugins are included in your editor for these rules to apply.

Deserialization Properties in Plugins

Plugins can define how they handle HTML deserialization using properties within their parsers.html.deserializer configuration:

  • parse: A function ({ editor, element, getOptions, ... }) => Partial<SlateNode> that takes an HTML element and returns a partial Plate node. This is where the main conversion logic resides.
  • query: An optional function ({ element, getOptions }) => boolean that determines if the deserializer rule should even be considered for the current HTML element.
  • rules: An array of rule objects, each defining conditions for matching an HTML element:
    • validNodeName: String or array of strings for matching HTML tag names (e.g., 'P', ['STRONG', 'B']).
    • validAttribute: Object or array of objects specifying required attribute names and/or values (e.g., { align: ['left', 'center'] }).
    • validClassName: String or array of strings for matching CSS class names.
    • validStyle: Object or array of objects specifying required CSS style properties and/or values (e.g., { fontWeight: ['600', '700', 'bold'] }).
  • isElement: Boolean, true if the plugin deserializes an HTML element into a Plate Element node.
  • isLeaf: Boolean, true if the plugin deserializes an HTML element or style into a Plate Leaf (mark) on a Text node.
  • attributeNames: Array of HTML attribute names whose values should be preserved on the node.attributes property of the resulting Plate node.
  • withoutChildren: Boolean, if true, child nodes of the HTML element are not processed by convertHtmlAttributes.

Customizing Deserialization Behavior

You can extend a plugin to modify its HTML parsing logic. This is useful for supporting non-standard HTML attributes or structures.

lib/custom-code-block-plugin.ts
import { CodeBlockPlugin } from '@platejs/code-block/react';
import { CodeLinePlugin } from '@platejs/code-block'; // Base if needed
 
const MyCustomCodeBlockPlugin = CodeBlockPlugin.configure({
  parsers: {
    html: {
      deserializer: {
        // Inherit most rules and properties, then override or add
        ...CodeBlockPlugin.parsers.html.deserializer,
        parse: ({ element, editor }) => { // editor might be needed for getType
          const language = element.getAttribute('data-custom-lang') || element.className.match(/language-(?<lang>[^\s]+)/)?.groups?.lang;
          const textContent = element.textContent || '';
          const lines = textContent.split('\n');
 
          return {
            type: CodeBlockPlugin.key, // Or editor.getType(CodeBlockPlugin.key)
            lang: language,
            code: textContent, // Example: store full code string
            children: lines.map((line) => ({
              type: editor.getType(CodeLinePlugin.key),
              children: [{ text: line }],
            })),
          };
        },
        rules: [
          // Inherit existing rules if desired
          ...(CodeBlockPlugin.parsers.html.deserializer.rules || []),
          // Add a new rule to match based on a custom attribute
          { validAttribute: { 'data-custom-lang': true } },
        ],
      },
    },
  },
});
 
// Then use MyCustomCodeBlockPlugin in your editor configuration.

This example customizes CodeBlockPlugin to look for a data-custom-lang attribute or a language-* class for determining the code language.

Advanced Deserialization Example (ListPlugin)

The ListPlugin demonstrates a more complex deserialization scenario where it transforms HTML list structures (<li> elements) into indented paragraphs within Plate, using aria-level to determine indentation.

Here's a conceptual look at its deserialization logic:

// Simplified concept from ListPlugin
export const ListPluginConfig = {
  // ... other configurations ...
  parsers: {
    html: {
      deserializer: {
        isElement: true,
        // query: ({ element }) => hasListAncestor(element), // Example condition
        parse: ({ editor, element }) => ({
          type: 'p', // Converts <li> to <p>
          indent: Number(element.getAttribute('aria-level') || '1'),
          listStyleType: element.style.listStyleType || undefined,
          // Children are processed by Plate's default deserializer after this node is created
        }),
        rules: [
          { validNodeName: 'LI' }, // Only applies to <li> elements
        ],
      },
    },
  },
};

This illustrates how a plugin can completely reinterpret HTML structures into a different Plate representation.

API Reference

serializeHtml(editor, options)

Converts Plate nodes from editor.children (or a provided value) into an HTML string. This function is typically used server-side.

Parameters

Collapse all

    A server-side Plate editor instance, created via createSlateEditor with components configured.

    Options for serialization.

OptionsSerializeHtmlOptions<P = PlateStaticProps>

Collapse all

    A React component to wrap the entire editor content during static rendering. Defaults to PlateStatic. The component receives editor, value, and any props passed here.

    Props to pass to the editorComponent. P defaults to PlateStaticProps.

    Plate nodes to serialize. If not provided, editor.children will be used.

    An array of class name prefixes to preserve if stripClassNames is true. null preserves all if stripClassNames is true. Default: ['slate-', 'line-clamp'].

    If true, removes all class names from the output HTML except those whose prefixes are listed in preserveClassNames. Default: true.

    If true, removes all data-* attributes from the output HTML. Default: true.

Returns

Collapse all

    A promise that resolves to the serialized HTML string.


api.html.deserialize(options)

Parses an HTML string or HTMLElement into a Plate Value (an array of Descendant nodes). This is typically used on the client-side with a fully configured Plate editor.

Parameters

Collapse all

    The client-side Plate editor instance.

    Options for deserialization.

OptionsDeserializeHtmlOptions

Collapse all

    The HTML string or HTMLElement to deserialize.

    If true (default), collapses whitespace from text nodes similarly to how browsers treat whitespace in HTML. Set to false to preserve all whitespace. Default: true.

    Deprecated. Use collapseWhiteSpace. If true, leading/trailing whitespace is trimmed and sequences of whitespace are collapsed. Default: true.

Returns

Collapse all

    The deserialized Plate Value.

Next Steps