Skip to main content

Overview

The @lexical/html package provides utilities for converting between Lexical editor state and HTML, enabling import from HTML strings and export to HTML format.

Installation

npm install @lexical/html

Core Functions

$generateNodesFromDOM

Converts DOM nodes to Lexical nodes.
function $generateNodesFromDOM(
  editor: LexicalEditor,
  dom: Document | ParentNode
): Array<LexicalNode>
editor
LexicalEditor
required
The editor instance (used for node type registration)
dom
Document | ParentNode
required
The DOM document or element to convert from
Returns: Array of Lexical nodes Example:
import { $generateNodesFromDOM } from '@lexical/html';

// In browser
const parser = new DOMParser();
const dom = parser.parseFromString(htmlString, 'text/html');

editor.update(() => {
  const nodes = $generateNodesFromDOM(editor, dom);
  $getRoot().append(...nodes);
});
Headless Example (Node.js):
import { $generateNodesFromDOM } from '@lexical/html';
import { JSDOM } from 'jsdom';

const jsdom = new JSDOM(htmlString);
const dom = jsdom.window.document;

editor.update(() => {
  const nodes = $generateNodesFromDOM(editor, dom);
  $getRoot().append(...nodes);
});

$generateHtmlFromNodes

Converts Lexical nodes to HTML string.
function $generateHtmlFromNodes(
  editor: LexicalEditor,
  selection?: BaseSelection | null
): string
editor
LexicalEditor
required
The editor instance
selection
BaseSelection | null
Optional selection to export only selected nodes
Returns: HTML string Example:
import { $generateHtmlFromNodes } from '@lexical/html';

// Export entire editor
editor.getEditorState().read(() => {
  const html = $generateHtmlFromNodes(editor);
  console.log(html);
});

// Export only selection
editor.getEditorState().read(() => {
  const selection = $getSelection();
  const html = $generateHtmlFromNodes(editor, selection);
  console.log(html);
});

How It Works

Import (HTML → Lexical)

  1. Parses HTML string into DOM using DOMParser (browser) or JSDOM (Node.js)
  2. Traverses DOM nodes and calls importDOM() on registered Lexical node classes
  3. Creates corresponding Lexical nodes based on conversion functions
  4. Maintains hierarchy and attributes during conversion
Supported Conversions: The conversion depends on the importDOM() method of registered nodes:
  • <p> → ParagraphNode
  • <h1>-<h6> → HeadingNode (from @lexical/rich-text)
  • <blockquote> → QuoteNode (from @lexical/rich-text)
  • <ul>, <ol> → ListNode (from @lexical/list)
  • <li> → ListItemNode (from @lexical/list)
  • <a> → LinkNode (from @lexical/link)
  • <code>, <pre> → CodeNode (from @lexical/code)
  • <table>, <tr>, <td> → TableNode, TableRowNode, TableCellNode (from @lexical/table)
  • Text nodes → TextNode with formatting
  • <br> → LineBreakNode
  • <b>, <strong> → Bold format
  • <i>, <em> → Italic format
  • <u> → Underline format
  • <s>, <strike>, <del> → Strikethrough format

Export (Lexical → HTML)

  1. Traverses Lexical node tree from root
  2. Calls exportDOM() on each node to get HTML element
  3. Appends child HTML elements recursively
  4. Returns final HTML string
Conversion Examples:
  • ParagraphNode → <p>
  • HeadingNode → <h1>-<h6>
  • QuoteNode → <blockquote>
  • ListNode → <ul> or <ol>
  • ListItemNode → <li>
  • LinkNode → <a href="...">
  • TextNode → Text with <b>, <i>, <u>, etc. for formatting
  • LineBreakNode → <br>

DOM Conversion Customization

Nodes can customize HTML import/export by implementing:

Custom Import

static importDOM(): DOMConversionMap | null {
  return {
    div: (node: Node) => ({
      conversion: convertDivElement,
      priority: 0
    })
  };
}

Custom Export

exportDOM(editor: LexicalEditor): DOMExportOutput {
  const element = document.createElement('div');
  element.setAttribute('data-custom', 'value');
  return { element };
}

Complete Example

import { createEditor } from 'lexical';
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { CodeNode } from '@lexical/code';
import { LinkNode } from '@lexical/link';
import { ListNode, ListItemNode } from '@lexical/list';

const editor = createEditor({
  nodes: [
    HeadingNode,
    QuoteNode,
    CodeNode,
    LinkNode,
    ListNode,
    ListItemNode
  ]
});

editor.setRootElement(document.getElementById('editor'));

// Import HTML
const htmlString = `
  <h1>Hello World</h1>
  <p>This is <b>bold</b> and <i>italic</i> text.</p>
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
  </ul>
  <a href="https://lexical.dev">Lexical</a>
`;

const parser = new DOMParser();
const dom = parser.parseFromString(htmlString, 'text/html');

editor.update(() => {
  const nodes = $generateNodesFromDOM(editor, dom);
  $getRoot().select();
  $getRoot().clear();
  $getRoot().append(...nodes);
});

// Export HTML
function exportHtml() {
  return editor.getEditorState().read(() => {
    return $generateHtmlFromNodes(editor);
  });
}

const html = exportHtml();
console.log(html);

Headless (Server-Side) Example

import { createHeadlessEditor } from '@lexical/headless';
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
import { JSDOM } from 'jsdom';

// Set up JSDOM
const jsdom = new JSDOM('<!DOCTYPE html>');
global.document = jsdom.window.document;
global.window = jsdom.window as any;

const editor = createHeadlessEditor({
  nodes: [/* ... */]
});

// Import HTML
const htmlInput = '<h1>Server-side HTML</h1><p>Processing...</p>';
const dom = new JSDOM(htmlInput).window.document;

editor.update(() => {
  const nodes = $generateNodesFromDOM(editor, dom);
  $getRoot().append(...nodes);
});

// Process and export
const htmlOutput = editor.getEditorState().read(() => {
  return $generateHtmlFromNodes(editor);
});

console.log(htmlOutput);

Common Use Cases

Paste HTML from Clipboard

import { $generateNodesFromDOM } from '@lexical/html';

editor.registerCommand(
  PASTE_COMMAND,
  (event: ClipboardEvent) => {
    const html = event.clipboardData?.getData('text/html');
    if (html) {
      const parser = new DOMParser();
      const dom = parser.parseFromString(html, 'text/html');
      
      editor.update(() => {
        const nodes = $generateNodesFromDOM(editor, dom);
        const selection = $getSelection();
        if ($isRangeSelection(selection)) {
          selection.insertNodes(nodes);
        }
      });
      
      return true;
    }
    return false;
  },
  COMMAND_PRIORITY_HIGH
);

Export Selection as HTML

import { $generateHtmlFromNodes } from '@lexical/html';

function copySelectionAsHtml() {
  editor.getEditorState().read(() => {
    const selection = $getSelection();
    const html = $generateHtmlFromNodes(editor, selection);
    
    // Copy to clipboard
    navigator.clipboard.writeText(html);
  });
}

Load Initial Content from HTML

import { $generateNodesFromDOM } from '@lexical/html';

function loadInitialContent(htmlString: string) {
  const parser = new DOMParser();
  const dom = parser.parseFromString(htmlString, 'text/html');
  
  editor.update(() => {
    const nodes = $generateNodesFromDOM(editor, dom);
    $getRoot().clear().append(...nodes);
  });
}

Build docs developers (and LLMs) love