# Technical Documentation: facebook/lexical

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/facebook/lexical)

### Overview (`README.md`)

<p align="center">
  <a href="https://lexical.dev"><picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://lexical.dev/img/logo-dark.svg" />
  <source media="(prefers-color-scheme: light)" srcset="https://lexical.dev/img/logo.svg" />
  <img alt="Lexical" src="https://lexical.dev/img/logo.svg" height="60" />
</picture></a>
</p>

<p align="center">
  An extensible text editor framework that provides excellent reliability, accessibility and performance.
</p>

<p align="center">
  <a href="https://www.npmjs.com/package/lexical"><img alt="NPM Version" src="https://img.shields.io/npm/v/lexical?color=43be15&label="/></a>
  <a href="https://www.npmjs.com/package/lexical"><img alt="NPM Downloads" src="https://img.shields.io/npm/dw/lexical"/></a>
  <a href="https://github.com/facebook/lexical/actions/workflows/tests.yml"><img alt="Build Status" src="https://img.shields.io/github/actions/workflow/status/facebook/lexical/tests.yml"/></a>
  <a href="https://discord.gg/KmG4wQnnD9"><img alt="Discord" src="https://img.shields.io/discord/953974421008293909"/></a>
</p>

<p align="center">
  <a href="https://lexical.dev/docs/intro">Documentation</a> | <a href="https://lexical.dev/docs/getting-started/quick-start">Getting Started</a> | <a href="https://playground.lexical.dev">Playground</a> | <a href="https://lexical.dev/gallery">Gallery</a>
</p>

<br />

## Features

- **Framework Agnostic Core** - Works with any UI framework, with official [React bindings](https://lexical.dev/docs/getting-started/react)
- **Reliable & Accessible** - Built-in accessibility support and WCAG compliance
- **Extensible** - Plugin-based architecture with powerful extension points
- **Immutable State Model** - Time-travel ready with built-in undo/redo
- **Collaborative Editing** - Real-time collaboration via [Yjs](https://github.com/yjs/yjs) integration
- **Serialization** - Import/export from JSON, Markdown, and HTML
- **Rich Content** - Support for tables, lists, code blocks, images, and custom nodes
- **Cross-browser** - Firefox 115+, Safari 15+, Chrome 86+ (see [Supported Browsers](https://lexical.dev/docs/getting-started/supported-browsers))
- **Type Safe** - Written in TypeScript with comprehensive type definitions

## Quick Start

For framework-agnostic usage, start with the
[Vanilla JS quick start](https://lexical.dev/docs/getting-started/quick-start)
or the
[Vanilla JS example](https://stackblitz.com/github/facebook/lexical/tree/main/examples/vanilla-js?file=src%2Fmain.ts).

For React applications, install Lexical with the official React bindings:

```bash
npm install lexical @lexical/react
```

```jsx
import { $getRoot, $getSelection } from 'lexical';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';

const initialConfig = {
  namespace: 'MyEditor',
  onError: (error) => console.error(error),
};

function Editor() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      <PlainTextPlugin
        contentEditable={<ContentEditable />}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <HistoryPlugin />
    </LexicalComposer>
  );
}
```

Try it yourself:
- [Plain Text Example](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-plain-text?file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview)
- [Rich Text Example](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-rich?file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview)

## Development

```bash
# Install dependencies
pnpm install

# Start playground dev server
pnpm run start

# Run tests
pnpm run test-unit
pnpm run test-e2e-chromium

# Lint and type check
pnpm run ci-check
```

See [CONTRIBUTING.md](https://github.com/facebook/lexical/blob/main/CONTRIBUTING.md) for detailed development guidelines.

## Documentation

- **User Guide**: [lexical.dev/docs/intro](https://lexical.dev/docs/intro)
- **API Reference**: [lexical.dev/docs/api](https://lexical.dev/docs/api/modules/lexical)
- **Developer Guide**: [AGENTS.md](https://github.com/facebook/lexical/blob/main/AGENTS.md) - Architecture and development workflows
- **Examples**: [examples/](https://github.com/facebook/lexical/tree/main/examples) - Sample implementations

## Community & Support

- **Discord**: Join our [Discord server](https://discord.gg/KmG4wQnnD9) for announcements, questions, discussions
- **Issues**: Report bugs and request features on [GitHub Issues](https://github.com/facebook/lexical/issues)
- **Release Notes**: Release notes are available in [Github Releases](https://github.com/facebook/lexical/releases)

## Browser Support

| Browser | Version |
|---------|---------|
| Chrome  | 86+     |
| Firefox | 115+    |
| Safari  | 15+     |
| Edge    | 86+     |

## Contributors

We welcome contributions! Please read our [Contributing Guide](https://github.com/facebook/lexical/blob/main/CONTRIBUTING.md) to learn about our development process and how to propose bugfixes and improvements.

<a href="https://github.com/facebook/lexical/graphs/contributors"><img src="https://contrib.rocks/image?repo=facebook/lexical" /></a>

## License

[MIT](https://github.com/facebook/lexical/blob/main/LICENSE) License Copyright ©2026 Meta Platforms, Inc.

---

### Overview (`dev-examples/dom-import/README.md`)

# DOMImportExtension example

A reduced rich-text editor focused on demonstrating the new
`DOMImportExtension` import pipeline. Each node-providing extension
registers its own import rules, so listing the runtime extensions is
all the configuration the pipeline needs —
`ClipboardDOMImportExtension` is the only import-specific dependency:

- **Rich text** — paragraphs, headings, quotes (`RichTextExtension`,
  which also registers the `<h1>`–`<h6>` / `<blockquote>` import
  rules and the shared `CoreImportExtension` baseline).
- **Lists** — bullet / numbered / check-list (`ListExtension`).
- **Tables** (`TableExtension`).
- **Links** (`LinkExtension`).
- **Horizontal rules** (`HorizontalRuleExtension` — the `<hr>` rule
  ships with `CoreImportExtension`, gated on the node being
  registered).
- **Code blocks with Shiki highlighting** (`CodeShikiExtension`
  registers `CodeNode` / `CodeHighlightNode` and wires Shiki up as
  the syntax-highlighting tokenizer; the underlying `CodeExtension`
  brings the import rules — `<pre>`, multi-line `<code>`, GitHub
  raw-file-view code tables, monospace `<div>`, and the VS Code
  paste consolidation preprocess).
- **Markdown shortcuts** — type `# `, `* `, `1. `, `> `, ``` ``` ```
/* Detailed source-code truncated for AI context efficiency. */
```sh
pnpm run start:dev-example dom-import
```

(or `pnpm -F @lexical/dev-dom-import-example dev` directly.)

---

### Overview (`dev-examples/hmr/README.md`)

# Lexical HMR Example

A minimal [Vite](https://vitejs.dev/) + React app that demonstrates
`HMRExtension` from `@lexical/extension`. Editor content, the editable flag, and undo/redo history (via
`HistoryExtension`) are preserved across hot module reloads.

## Running

From the repository root:

```sh
pnpm install
pnpm -C dev-examples/hmr dev
```

Then open the printed URL, type something, and edit `src/App.tsx`. The editor
state survives the reload.

---

### Overview (`dev-examples/mdast-editor/README.md`)

# Mdast Editor (dev example)

A WYSIWYG Markdown editor with an editable Markdown source pane
(synchronized in both directions, exercising import and export), built on
the unreleased
[`@lexical/mdast`](../../packages/lexical-mdast) package — the
micromark/mdast-based alternative to `@lexical/markdown`. It is a port of
[`examples/markdown-editor`](../../examples/markdown-editor), which is the
same app built on the legacy bespoke implementation; comparing the two shows
what the extension-first design removes.

```bash
pnpm run start:dev-example mdast-editor
```

[](https://stackblitz.com/github/facebook/lexical/tree/main?file=dev-examples%2Fmdast-editor%2Fsrc%2FEditor.tsx&startScript=start:dev-example:mdast-editor)

Because this example depends on unreleased workspace packages, the
StackBlitz link opens the monorepo root and boots the example with the
`start:dev-example:mdast-editor` script.

Deployed builds of every dev example are served from the lexical
website at `/dev-examples/<name>/` (this one at
[/dev-examples/mdast-editor/](https://lexical.dev/dev-examples/mdast-editor/)),
including on every Cloudflare/Vercel deploy preview. To build them
locally: `pnpm run build && pnpm run build:dev-examples` at the
monorepo root, which outputs into
`packages/lexical-website/static/dev-examples/`. These are built from
the development artifacts on purpose — they are demos, and the
development builds keep Lexical's full error messages.

## Repro links

The document can be seeded from the URL hash (mirroring the playground's
`#doc=` convention), which makes bug reports reproducible with a plain
link. Two encodings, so a repro can use whichever fits:

- `#md=<encodeURIComponent(markdown)>` — the Markdown source, hand-editable
  directly in the URL:
  `/dev-examples/mdast-editor/#md=%23%20Repro%0A%0A*hello*`
- `#doc=<base64url(gzip(SerializedDocument))>` — the full editor state
  JSON (via `@lexical/file`), for node/state shapes Markdown can't
  express.

The **Share MD** / **Share JSON** buttons in the Markdown pane header put
the current document in the URL (and on the clipboard) in each encoding.
While a hash document is shown, localStorage persistence is suspended so a
repro link never clobbers locally persisted work; **Reset** drops the hash
and resumes normal persistence.

## What got simpler vs. `examples/markdown-editor`

The legacy example's `MarkdownExtension.ts` is ~240 lines; the equivalent
[`MdastEditorExtension.ts`](./src/extensions/MdastEditorExtension.ts) here is
example plumbing only (the preview signal and two toolbar commands), because
`@lexical/mdast` ships extension-ready:

- **No transformer list to curate.** Depending on `MdastShortcutsExtension`
  pulls in the CommonMark + GFM grammar and every node it needs.
- **No `registerMarkdownShortcuts` call.** The extension registers the
  streaming shortcuts itself, driven by the same micromark grammar as
  document import.
- **No checklist workaround.** The legacy example needed a custom
  `text-match` transformer (`CHECK_LIST_ITEM`, ~50 lines) so `[ ] ` typed in
  an existing bullet item would convert; that behavior is built in.
- **No newline-handling configuration.** `shouldPreserveNewlines` /
  `mergeAdjacentNewlines` don't exist; micromark parses CommonMark as-is
  and the original syntax is preserved on the nodes via `NodeState`.

It also does more: blockquotes, links, strikethrough, thematic breaks, and
fenced code blocks round-trip out of the box (the legacy example's default
set covered headings, lists, and inline formatting), and the code blocks
are syntax-highlighted by adding `CodeShikiExtension` — highlighting
composes with the Markdown machinery without any extra wiring.

## Bundle size

The flip side of parsing with micromark is bundle weight. Build the
production dist artifacts first, then run the comparison:

```bash
pnpm run build-prod   # at the monorepo root
npm run size          # in this directory
```

It builds functionally-equivalent headless bundles — editor + markdown
node set + typing shortcuts + import/export — one per implementation, from
the production dist artifacts, and prints their sizes. All include the
lexical core, so the deltas are the markdown machinery itself. Snapshot at
the time of writing:

| bundle                     | minified  | min+gzip |
| -------------------------- | --------- | -------- |
| legacy `@lexical/markdown` | 287.3 kB  | 77.3 kB  |
| `@lexical/mdast`           | 408.6 kB  | 103.6 kB |
| delta (full vs legacy)     | +121.4 kB | +26.3 kB |

The dist build keeps the micromark/mdast dependencies external, so the app
bundler resolves them with browser export conditions and tree-shakes what's
unused.

---

### Overview (`examples/agent-example/README.md`)

# AI Agent Example

A React + Tailwind CSS rich text editor with AI agent functionality powered by [transformers.js](https://huggingface.co/docs/transformers.js). The AI runs entirely in the browser using WebAssembly — no server required.

Based on the [website-toolbar](../website-toolbar) example with added AI capabilities.

## Features

- **Rich text editing** with block types (headings, quotes), formatting (bold, italic, underline), and alignment
- **AI Generate** — click "Generate" to have the AI write a new paragraph, streamed token-by-token into the editor
- **Extract Entities** — detect people, places, and organizations in the text and replace them with interactive decorator nodes (PlaceNode links to Google Maps, PersonNode and OrgNode link to Google search)
- **Abort** — click "Stop" or press Escape to cancel an in-progress AI operation
- **Dark mode** toggle

## How it works

Uses two models running via `@huggingface/transformers` in a Web Worker with the WASM backend for broad compatibility, including Safari on iOS:

- [SmolLM2-135M-Instruct](https://huggingface.co/HuggingFaceTB/SmolLM2-135M-Instruct) (q4 quantized, ~70MB) for text generation
- [Xenova/bert-base-NER](https://huggingface.co/Xenova/bert-base-NER) for named entity recognition (people, places, organizations)

Models are loaded lazily on first use. Generated text streams directly into the editor as tokens arrive. Extracted entities are converted into color-coded decorator nodes with contextual links.

> **Note:** SmolLM2-135M is a small language model. Results are best-effort and meant as a demonstration of browser-based AI integration with Lexical.

## Run it locally

```bash
pnpm i && pnpm run dev
```

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/agent-example?file=src/main.tsx)

---

### Overview (`examples/extension-react-table/README.md`)

# React Table Extension example

Here we have simplest Lexical Table Plugin setup in rich text configuration (`@lexical/rich-text`) with history (`@lexical/history`) and accessibility (`@lexical/dragon`) features enabled, built with Lexical Extension.

**Run it locally:** `pnpm i && pnpm run dev`


[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/extension-react-table?file=src%2Fmain.tsx)

---

### Overview (`examples/extension-sveltekit-ssr-hydration/README.md`)

# Svelte 5 + Lexical Extension Demo

This example demonstrates how you can use Lexical Extension signals
reactively with a `$` prefix because they comply with the Svelte Store API.

This example shows how you might do SSR to prerender the editor's HTML
and hydrate it client-side directly from the prerendered HTML (so you're not
sending the content twice). Note that this only works well when your importDOM
is lossless relative to the createDOM output.

It's also a rudimentary demo for how you can support Vite HMR to preserve
the editor's state during reloads (great for tweaking html/styles/etc.)

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/extension-sveltekit-ssr-hydration?file=src%2Froutes%2F%2Bpage.svelte)

---

### Overview (`examples/extension-vanilla-react-plugin-host/README.md`)

# Vanilla JS Extension with a React plug-in host

Demonstrates using Lexical Extension to build a very basic checklist editor with Vanilla JS, using Tailwind for CSS, and rendering a React-based extension (`TreeViewExtension`) into the page with `ReactPluginHostExtension`.

- The editor is built with `buildEditorFromExtensions`, from `RichTextExtension`, `HistoryExtension`, `CheckListExtension` and others.
- `ReactPluginHostExtension` owns a React root. `mountReactPluginHost` mounts it, and `mountReactExtensionComponent` renders the tree view into `#tree-view`, so the app needs React at runtime but no JSX.
- `configExtension(TreeViewExtension, {...})` styles the tree view with Tailwind classes.
- `TailwindExtension` from `@lexical/tailwind` supplies the editor theme. `src/styles.css` points Tailwind at that package with `@source` so the classes it uses are generated.

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/extension-vanilla-react-plugin-host?file=src%2Fmain.ts)

---

### Overview (`examples/extension-vanilla-tailwind/README.md`)

# Basic Vanilla JS Extension with Tailwind example

Demonstrates using Lexical Extension to build a very basic checklist editor with Vanilla JS (no React), using Tailwind for CSS.

- The editor is built with `buildEditorFromExtensions`, from `RichTextExtension`, `HistoryExtension`, `CheckListExtension` and others.
- `TailwindExtension` from `@lexical/tailwind` supplies a theme of Tailwind classes. `src/styles.css` points Tailwind at that package with `@source` so the classes it uses are generated.
- The "Editor state" panel is kept in sync by a small extension that reads the `EditorStateExtension` signal.
- `LazyExtension` shows how an extension can load more code with a dynamic `import()` and still clean up when the editor is disposed.

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/extension-vanilla-tailwind?file=src%2Fmain.ts)

---

### Overview (`examples/markdown-editor/README.md`)

# Markdown Editor example

This example demonstrates a split-view markdown editor: a WYSIWYG editor on
the left synchronized with a live markdown preview on the right. It is built
entirely on the Lexical [Extension](https://lexical.dev/docs/concepts/extensions)
system — every piece of state and behavior the React UI relies on is provided
by an extension as a command, an output signal, or both.

## Layout

- **`src/extensions/MarkdownExtension.ts`** — pulls in `RichTextExtension`,
  `HistoryExtension`, `ListExtension`, `CheckListExtension`,
  `TabIndentationExtension` and `EditorStateExtension`; registers
  `registerMarkdownShortcuts` with the curated `MARKDOWN_TRANSFORMERS`
  array (headings, ordered/unordered/check lists, inline code, bold,
  italics, bold-italic) so that typing `# `, `**bold**`, `` `code` ``,
  `- item`, `1. item`, `[ ] todo` etc. transforms inline. The
  transformer list keeps `CHECK_LIST` ahead of `UNORDERED_LIST` so that
  `- [ ] foo` matches the checklist regex first on import. A custom
  `CHECK_LIST_ITEM` `TextMatchTransformer` (trigger `' '`) handles the
  typing-time case where the user has already typed `- ` (turning the
  line into a bullet list item) and then types `[ ] ` — the standard
  element transformer can't fire there because its parent is no longer
  a paragraph at root, but the text-match transformer has no such
  restriction. The extension also registers `FORMAT_PARAGRAPH_COMMAND`
  and `FORMAT_HEADING_COMMAND<HeadingTagType>`, and exposes the
  current document as a `markdown` `computed` signal derived off
  `EditorStateExtension`.
- **`src/extensions/MarkdownPersistenceExtension.ts`** — owns the
  localStorage ↔ editor sync. It provides the editor's
  `$initialEditorState` so the document is seeded from
  `localStorage[storageKey]` (falling back to the configured
  `defaultMarkdown`). An effect on `MarkdownExtension`'s `markdown`
  signal writes back on every change, and `RESET_MARKDOWN_COMMAND`
  clears the storage entry and re-imports the default in place — no
  page reload.
- **`src/extensions/ToolbarStateExtension.ts`** — exposes everything
  the toolbar reads as output signals: `canUndo` / `canRedo` are
  `computed` off `HistoryExtension`'s `historyState`, while
  `blockType`, `isBold`, `isItalic`, and `isCode` are `computed` off
  the editor-state signal. The React component calls
  `useExtensionSignalValue` for each signal and dispatches commands —
  no local `useState` / `useEffect` and no second update listener.
- **`src/plugins/ToolbarPlugin.tsx`** — block-type select (paragraph,
  heading 1-3, bullet/numbered/check list), undo / redo, and bold /
  italic / inline-code buttons. Pure UI: dispatches commands, reads
  signals.
- **`src/plugins/MarkdownPreviewPlugin.tsx`** — one-liner that reads
  the markdown signal via `useExtensionSignalValue` and renders it.
- **`src/Editor.tsx`** — composes the extensions through
  `LexicalExtensionComposer`, passing the storage key and demo
  document as `MarkdownPersistenceExtension` config.

## Tests

`pnpm run test` runs vitest. The suite covers:

- Round-tripping every supported feature through the markdown
  transformers (`$convertFromMarkdownString` →
  `$convertToMarkdownString`).
- That `CHECK_LIST` matches `- [x] foo` ahead of `UNORDERED_LIST` on
  import, and that the new `CHECK_LIST_ITEM` text-match transformer
  flips a bullet list into a checklist when the user types `[ ] ` /
  `[x] ` inside an existing list item (simulated character-by-character
  via `selection.insertText`).
- That `MarkdownExtension`'s `markdown` output signal updates as the
  editor state changes, and that `ToolbarStateExtension`'s `canUndo` /
  `canRedo` / `blockType` / `isBold` / `isItalic` / `isCode` signals
  follow real edits, real `UNDO_COMMAND` / `REDO_COMMAND` /
  `FORMAT_TEXT_COMMAND` dispatches, and selection changes.

## Running

```bash
pnpm install
pnpm run dev        # vite dev server
pnpm run build      # production build
pnpm run typecheck  # tsc --noEmit
pnpm run test       # vitest run
```

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/markdown-editor?file=src/main.tsx)

---

### Overview (`examples/nextjs-code-shiki/README.md`)

# Next.js + `@lexical/code-shiki` example

A minimal [Next.js](https://nextjs.org/) app that drives a Lexical rich-text
editor with Shiki-based code highlighting, wired entirely through the
Lexical [Extension](https://lexical.dev/docs/concepts/extensions) system.

## What it demonstrates

- Using `LexicalExtensionComposer` (from `@lexical/react/LexicalExtensionComposer`)
  with `RichTextExtension`, `HistoryExtension`, `AutoFocusExtension`, and a
  small example-owned `CodeShikiDemoExtension` that pulls in
  `CodeShikiExtension` as a dependency. No legacy `LexicalComposer` /
  `*Plugin` wrappers.
- Driving `@lexical/code-shiki`'s public API from the extension:
  - `getCodeLanguageOptions()` to seed the editor with the language list
    that shiki has bundled metadata for, via the extension's
    `$initialEditorState` hook.
  - `loadCodeLanguage('python')` from the extension's `register` hook to
    exercise shiki's dynamic `@shikijs/langs/<lang>` import path through
    Next.js' bundler. Once the promise resolves and `isCodeLanguageLoaded`
    confirms it, the extension appends `Loaded: python` to the document.

## Why this example exists

This is also the release-artifact integration test for `@lexical/code-shiki`.
`scripts/__tests__/integration/prepare-release.test.mjs` globs
`examples/*/package.json`, installs each one against the freshly built
`npm/*.tgz` tarballs, runs `npm run build` (so Next.js bundles the example
against the published `@lexical/code-shiki`), then runs `npm run test`
(Playwright) against the production build. The Playwright assertions
confirm:

1. `Registered:.*typescript` — the synchronous `bundledLanguagesInfo` list
   resolved through the published bundle.
2. `Loaded: python` — the dynamic `import('@shikijs/langs/python')`
   inside shiki resolved at runtime, which only works if
   `shiki` / `@shikijs/*` are **external** in the published
   `@lexical/code-shiki` bundle rather than inlined by Rollup.

## Local development

```bash
pnpm install
pnpm run dev        # next dev on http://localhost:3000
pnpm run build      # next build
pnpm run start      # next start (used by playwright.config.ts)
pnpm run test       # playwright tests against the built app
```

---

### Overview (`examples/node-replacement/README.md`)

# Node Replacement Example

A rich text editor built with extensions (`RichTextExtension`, `HistoryExtension`, and `AutoFocusExtension`) that uses node replacement to swap every `ParagraphNode` for a `CustomParagraphNode`. The replacement is declared in the `nodes` of `CustomParagraphExtension`.

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-replacement?file=src/main.tsx)

---

### Overview (`examples/node-state-review/README.md`)

# Paragraph review with NodeState

This example adds a `reviewed` flag to existing paragraph nodes. `DOMRenderExtension` renders and exports the flag as `data-reviewed="true"`; `DOMImportExtension` restores it from HTML. NodeState handles JSON serialization and cloning without a custom node class.

Run `npm install` and `npm run dev` to start the standalone example. From the Lexical repository, run `pnpm build` and `pnpm exec vite examples/node-state-review -c examples/node-state-review/vite.config.monorepo.ts` to use the local packages.

Place the cursor in a paragraph and choose **Toggle reviewed**. Use **Export HTML**, edit the HTML, then choose **Import HTML** to try the round trip. Expand **Editor state JSON** to see the stored data.

---

### Overview (`examples/node-state-style/README.md`)

# Node State Style example

Here we have an example that demonstrates how NodeState can be used with a
DOMRenderExtension to override the create and export behavior of any node, and a
DOMImportExtension to capture arbitrary inline `style` properties when importing
HTML.

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-state-style?file=src/main.tsx)

---

### Overview (`examples/react-mathtype/README.md`)

# React MathType example

This example demonstrates a standalone WIRIS MathType integration for a
Lexical React editor. It keeps MathType as an example dependency and stores
formulas in a custom Lexical node instead of allowing MathType to mutate the
Lexical contenteditable directly.

The bridge is packaged as `MathTypeExtension`. It uses
`@wiris/mathtype-generic` to open the MathType/ChemType UI, converts the
generated MathML image into `MathTypeNode` state inside `editor.update`, and
reopens MathType when a formula node is double-clicked.

MathType stores formulas as MathML. The rendered formula image is kept in node
state for the demo, while DOM export emits the same `img.Wirisformula` shape
that MathType's parser expects.

## How the pieces fit together

`MathTypeExtension.build` owns the mutable bridge state — the MathType
integration instance and the node the open dialog is editing — for the
lifetime of one editor, and exposes two functions over it:

- `mountIntegration(target, toolbar)` creates the MathType integration and
  returns its teardown function. `MathTypeIntegrationComponent` calls it from
  an effect and renders the toolbar plus the offscreen element MathType uses
  to anchor its dialog.
- `editFormula(nodeKey, formula)` reopens MathType for an existing node. The
  `MathTypeNode` decorator reaches it with
  `useExtensionDependency(MathTypeExtension)`.

Using the extension's own output rather than a React context means the
extension does not have to override `ReactExtension`'s
`EditorChildrenComponent`, which is a single, last-one-wins slot: taking it
would silently break any other extension in the editor that needs it.

## Running

```bash
pnpm install
pnpm run dev
pnpm run build
pnpm run typecheck
```

MathType may require a WIRIS license or self-hosted services for production
use. See the MathType generic integration documentation for service
configuration details. Out of the box the integration calls WIRIS' hosted demo
services at `www.wiris.net`, so the editor needs network access to them to
render formulas.

`@wiris/mathtype-generic` is imported eagerly at module scope, because it
installs the `window.WirisPlugin` singleton that `MathTypeNode` needs in order
to render a formula on the first paint. That puts the whole MathType bundle in
the initial chunk, which is why `vite build` reports a chunk over 500 kB.

---

### Overview (`examples/react-plain-text/README.md`)

# React Plain Text Lexical example

A plain text editor configured with one root extension. It composes `PlainTextExtension` and `HistoryExtension` with `LexicalExtensionComposer`. The debug panel uses `TreeViewExtension`.

[Getting started guide](https://lexical.dev/docs/getting-started/react)

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-plain-text?file=src/main.tsx)

---

### Overview (`examples/react-rich-collab/README.md`)

# Basic Vanilla JS example

Here we have simplest Lexical collaboration mode setup. Use it as a starting point for your own projects as well as platform for bug reporting!

**Run it locally:** `pnpm i && pnpm run dev:local`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-rich-collab?file=src/main.ts)

---

### Overview (`examples/react-rich/README.md`)

# React Rich Text example

A rich text editor configured with one root extension. It composes `RichTextExtension` and `HistoryExtension` with `LexicalExtensionComposer`. The debug panel uses `TreeViewExtension`.

[Getting started guide](https://lexical.dev/docs/getting-started/react)

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-rich?file=src/main.tsx)

---

### Overview (`examples/react-table/README.md`)

# React Table Plugin example

Here we have simplest Lexical Table Plugin setup in rich text configuration (`@lexical/rich-text`) with history (`@lexical/history`) and accessibility (`@lexical/dragon`) features enabled.

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-table?file=src/main.tsx)

---

### Overview (`examples/shadow-dom-web-component/README.md`)

# Lexical Web Component (Shadow DOM) Example

A framework-free [Vite](https://vitejs.dev/) app that packages a Lexical
rich-text editor as a **custom element** (`<lexical-editor>`) whose toolbar,
styles, and `contentEditable` all live inside an **open `ShadowRoot`** —
the scenario from
[facebook/lexical#2119](https://github.com/facebook/lexical/issues/2119),
[#6709](https://github.com/facebook/lexical/issues/6709), and
[#8125](https://github.com/facebook/lexical/issues/8125).

Where the sibling [`shadow-dom`](../shadow-dom) example demonstrates a React
app with the editor in a shadow root and the toolbar outside it, this one
demonstrates the inverse packaging: a fully self-contained web component.
The demo page mounts four instances — three light-DOM `<lexical-editor>`
hosts inside the form (a `required` notes editor, a themable summary editor,
and a pre-rendered editor that hydrates from `<template shadowrootmode="open">`)
plus a fourth instance inside a wrapper `<div>` that opens its own shadow root,
so the editor's contentEditable sits two shadow boundaries below the document
and exercises the multi-level walk through `getDOMShadowRoots`. Everything
runs on platform APIs only:

- `Element.attachShadow({mode: 'open'})` in `connectedCallback`, with the
  editor built by `@lexical/extension`'s `buildEditorFromExtensions` and torn
  down (`editor.dispose()`) in `disconnectedCallback`.
- **Form association** through the standard
  [`ElementInternals`](https://developer.mozilla.org/docs/Web/API/ElementInternals)
  API — each editor submits its serialized editor state with the surrounding
  `<form>`, no hidden `<input>` required.
- **`required` validation** through
  [`ElementInternals.setValidity`](https://developer.mozilla.org/docs/Web/API/ElementInternals/setValidity),
  driven by the editor's plain text content. A `<lexical-editor required>`
  participates in `form.checkValidity()` and the browser's native invalid-form
  UI just like a `<textarea required>`.
- **`disabled` and `readonly` attributes** that flip Lexical's editable
  state. `disabled` also drops the editor out of `FormData` and skips
  validation, matching `<input disabled>`; the
  [`formDisabledCallback`](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements#form-associated_callbacks)
  picks up an ancestor `<fieldset disabled>` automatically.
- **CSS custom property theming** — the editor exposes `--lexical-bg`,
  `--lexical-fg`, and a small palette of toolbar variables on its host
  element. The page redefines them to recolour the editor; inherited
  custom properties cross the shadow boundary on their own, so the
  internal layout stays private.
- **Toolbar slot** — `<button slot="toolbar-extra">` projects a
  light-DOM button into the editor's toolbar row. The button stays in
  the page (its click never crosses the boundary), but the page can drive
  the editor through the host's public API.
- **Floating selection popover** — the editor emits a composed
  `lexical-selection-rect` `CustomEvent` carrying the live viewport
  rect of the selection inside its shadow root, computed through
  Lexical's `getDOMSelectionRangeAndPoints`. A page-level
  [popover](https://developer.mozilla.org/docs/Web/API/Popover_API)
  positions itself from those coordinates and drives bold / italic /
  underline back through the host's editor.
- A composed `input` event that crosses the shadow boundary so the page can
  observe edits.
- Selection inside the shadow root is resolved by Lexical itself via
  `Selection.getComposedRanges` / `Selection.direction`, and focus via
  `ShadowRoot.activeElement`.

## Running

```sh
pnpm i && pnpm run dev
```

To run it against the Lexical sources in this repository instead of the
published packages, use `pnpm run monorepo:dev`.

Then open the printed URL. Try:

- Typing in any of the four editors and switching between them (each keeps
  its own selection and history).
- Selecting words with `Alt`/`Ctrl` + `Shift` + arrow keys, then using the
  in-shadow toolbar buttons — they reflect the selection's formats.
- Word/line deletion with `Alt`/`Ctrl` + `Backspace`/`Delete`.
- Submitting the form to see each editor's serialized state in the output.
- Submitting the form while the first editor (`required`) is empty: the
  browser blocks the submit and surfaces its native validation tooltip on
  the editor.
- Clicking the **Clear** button next to the notes toolbar — that button
  lives in the light DOM and is projected through the `toolbar-extra`
  slot, then drives the editor through the host's public API.
- Toggling the **Lock the summary editor** checkbox to flip the
  `readonly` attribute on the summary editor, then trying to type
  inside it: the contentEditable refuses input, but the form still
  submits the value.
- Selecting a few words in either editor — the floating B / I / U
  popover appears anchored under the selection (in the page, not the
  shadow root), and clicking it formats the text through the editor.
- Switching the OS or browser between light and dark mode (or
  redefining `--lexical-bg`, `--lexical-fg`, etc. in the page CSS) to
  see how page-side CSS variables retheme each editor without touching
  its shadow root.

## Tests

[Playwright](https://playwright.dev/) tests in [`tests/`](./tests) cover the
editors rendering in independent shadow roots (including the nested
editor inside the wrapper shadow root), typing and formatting, editor
independence, word deletion, `ElementInternals` form association, the
composed `input` event crossing the shadow boundary, and the floating
popover anchoring to a selection inside the nested shadow root. They
start the dev server automatically:

```sh
pnpm run test
```

## What's covered

This example aims to be a full reference a production user can copy out.
The Playwright suite covers each of the surfaces above plus a second
round of audit items:

- DOM-move state round-trip (`disconnectedCallback` caches the
  serialized state, `connectedCallback` restores it)
- `delegatesFocus: true` on the shadow root + `tabindex="0"` on the
  contentEditable
- `host.setCustomValidity()` + the standard `validity` / `willValidate`
  / `checkValidity` / `reportValidity` surface
- `formAssociatedCallback` + `host.form`, `formResetCallback`,
  `formDisabledCallback`, `formStateRestoreCallback` (bfcache / autocomplete)
- The standard `inert` attribute, `aria-label` / `aria-invalid` /
  `role="textbox"` mirroring
- A composed `lexical-validity-change` event for a visible error
  message
- `@media (prefers-color-scheme: dark)` / `(prefers-reduced-motion)` /
  `(forced-colors: active)` inside the shadow stylesheet
- Declarative shadow DOM (`<template shadowrootmode>`) — the third
  editor on the demo page pre-renders its shadow content and our
  `connectedCallback` reuses the existing `.content` element instead
  of creating a fresh contentEditable
- Hardened lifecycle: a duplicate `customElements.define` of the
  same tag throws `NotSupportedError` (the shipped helper guards
  against this); a host that fails to build doesn't crash the
  surrounding page

The playground e2e suite (`packages/lexical-playground/__tests__/e2e/ShadowDOM.spec.mjs`)
covers the corresponding playground-side surfaces: markdown shortcuts
(`# heading`, `- list`) and `@lexical/list` inside the shadow root,
`@lexical/history` undo/redo, the tree-view mirror, pointer events
(`composedPath` recovery for touch / pen / mouse), HTML paste
sanitization (a `<script>` tag is stripped), a large keyboard input
that keeps the reconciler responsive, image insert + paste,
NodeSelection on image click, blur + re-focus through the shadow
boundary, Korean and Chinese IME composition cycles, and yjs
convergence between two clients each rendered inside its own open
shadow root.

`lexical-devtools` descends through open shadow roots using the same
helpers this PR adds (`getDOMShadowRoots` / `getActiveElementDeep` /
`getEditorPropertyFromDOMNode`), so it resolves the shadow-mounted
editor without dev-example-side glue. Closed-mode shadow roots remain
opaque to a page-level integration by spec — the concept page documents
the limitation and the browser-unit suite verifies the helpers behave
correctly when a host attaches a closed root.

A handful of newer platform APIs are not exercised by this single
example because they slot in at the page layer rather than the editor
itself: the
[View Transitions API](https://developer.mozilla.org/docs/Web/API/View_Transitions_API)
animates between shadow-mounted instances on `document.startViewTransition`
without any editor-side glue, and the
[CSS Custom Highlight API](https://developer.mozilla.org/docs/Web/API/CSS_Custom_Highlight_API)
(`Highlight` + `::highlight()`) styles ranges that span the shadow
boundary as long as the page hands them un-retargeted boundary points —
the same shape `getDOMSelectionRangeAndPoints` already returns.

---

### Overview (`examples/shadow-dom/README.md`)

# Lexical Shadow DOM Example

A minimal [Vite](https://vitejs.dev/) + React app that demonstrates Lexical
across a Shadow DOM boundary in a _nested_ shape:

- An **outer editor** lives in the light DOM, with a `LexicalExtensionComposer`
  registering Rich Text, History, and Tab Indentation.
- An **inner editor** is mounted inside an open `ShadowRoot`, nested in the
  outer editor's tree via a `DecoratorNode` (`NestedEditorNode`).

The nesting exercises Lexical's platform-only shadow support:

- **Reading selection** uses
  [`Selection.getComposedRanges`](https://developer.mozilla.org/docs/Web/API/Selection/getComposedRanges)
  and [`Selection.direction`](https://developer.mozilla.org/docs/Web/API/Selection/direction),
  because `Selection.anchorNode` / `getRangeAt` are retargeted to the shadow
  host when the selection is inside a shadow tree.
- **Reading focus** uses
  [`ShadowRoot.activeElement`](https://developer.mozilla.org/docs/Web/API/DocumentOrShadowRoot/activeElement),
  because `document.activeElement` only reports the outermost shadow host.
- **Writing selection** uses the native `Selection.setBaseAndExtent`, and
  keyboard navigation / word + line deletion use the native `Selection.modify`,
  both of which operate on shadow-tree nodes directly.
- **Selection-change attribution** prefers the shadow-mounted candidate first
  when a `selectionchange` fires under a nested layout — so the inner editor
  wins attribution over its light-DOM parent.

## How it works

[`ShadowRoot.tsx`](./src/ShadowRoot.tsx) attaches an open shadow root to a host
`<div>` with `Element.attachShadow`, then portals its children (including the
inner editor's `contentEditable`) into the shadow tree with `createPortal`.
React context flows across the portal, so the inner editor is built exactly as
it would be in the light DOM — only its DOM lives behind the shadow boundary.
The inner editor's CSS is injected as a `<style>` element inside the shadow
root, since shadow trees do not inherit the document's stylesheets.

The `NestedEditorNode` in [`App.tsx`](./src/App.tsx) is a `DecoratorNode` whose
host sits in the outer editor's tree. Its `decorate()` returns a React subtree
that hosts the inner editor's `LexicalExtensionComposer` plus its shadow root —
so the inner editor lives at a real position inside the outer editor's
document.

The toolbar in [`Toolbar.tsx`](./src/Toolbar.tsx) lives in the light DOM and
dispatches commands (`FORMAT_TEXT_COMMAND`, undo/redo) that act on the _outer_
editor's selection.

## Running

```sh
pnpm i && pnpm run dev
```

To run it against the Lexical sources in this repository instead of the
published packages, use `pnpm run monorepo:dev`.

Then open the printed URL. Try:

- Typing in either editor — outer (light DOM) or inner (shadow root).
- Selecting words in the outer editor with `Alt`/`Ctrl` + `Shift` + arrow keys
  and pressing **Bold** / **Italic** / **Underline** in the toolbar.
- Word and line deletion with `Alt`/`Ctrl` + `Backspace`/`Delete` in either
  editor.

## Tests

[Playwright](https://playwright.dev/) tests in [`tests/`](./tests) cover
rendering both editors, typing across the shadow boundary, formatting an outer
selection via the light-DOM toolbar, and word deletion in the inner editor.
They start the dev server automatically:

```sh
pnpm run test
```

---

### Overview (`examples/vanilla-js-iframe/README.md`)

# Vanilla JS example in an iframe

A minimal Lexical editor built with `buildEditorFromExtensions` and no framework, using `RichTextExtension`, `HistoryExtension`, and `DragonExtension` (accessibility). The contentEditable lives inside an iframe, so the editor reads selection and focus from the iframe's window rather than the page's.

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/vanilla-js-iframe?file=src/main.ts)

---

### Overview (`examples/vanilla-js-plugin/README.md`)

# Basic Vanilla JS Extension example

An `EmojiExtension` that replaces smiles (`:)`, `:P`, etc...) with actual emojis (using [Node Transforms](https://lexical.dev/docs/concepts/transforms)) and uses own graphics for emojis rendering by creating our own custom node that extends [TextNode](https://lexical.dev/docs/concepts/nodes#textnode).

[Getting started guide](https://lexical.dev/docs/getting-started/creating-plugin)

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/vanilla-js-plugin?file=src/emoji-plugin/EmojiExtension.ts)

---

### Overview (`examples/vanilla-js/README.md`)

# Basic Vanilla JS example

A rich text editor configured with one root extension. It composes `RichTextExtension` and `HistoryExtension` with `buildEditorFromExtensions`.

[Getting started guide](https://lexical.dev/docs/getting-started/quick-start)

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/vanilla-js?file=src/main.ts)

---

### Overview (`examples/website-chat/README.md`)

# Website Chat example

Here we have an example of a chat with rich text (`@lexical/rich-text`), emoji picker, and history (`@lexical/history`). Each sent message is rendered in a separate lightweight read-only lexical instance.

This example is used on the page https://lexical.dev/

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/website-chat?file=src/main.tsx)

---

### Overview (`examples/website-notion/README.md`)

# Website Notion example

Here we have an example Lexical setup in rich text configuration (`@lexical/rich-text`) with history (`@lexical/history`), draggable blocks with menu based on experimental DraggableBlockPLugin (`@lexical/react/LexicalDraggableBlockPlugin`), slash commands with floating menu (`@lexical/react/LexicalTypeaheadMenuPlugin`)

This example is used on the page https://lexical.dev/

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/website-notion?file=src/main.tsx)

---

### Overview (`examples/website-rich-input/README.md`)

# Website Rich Input example

Here we have an example Lexical setup in rich text configuration (`@lexical/rich-text`) with history (`@lexical/history`), hashtag (`@lexical/hashtag`) and custom character counter plugin

This example is used on the page https://lexical.dev/

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/website-rich-input?file=src/main.tsx)

---

### Overview (`examples/website-toolbar/README.md`)

# Website Toolbar example

This example demonstrates the implementation of a toolbar with a set of buttons that execute commands and set text formats for the selection. Buttons can be active depending on where the selection is set

This example is used on the page https://lexical.dev/

**Run it locally:** `pnpm i && pnpm run dev`

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/website-toolbar?file=src/main.tsx)

---

### Overview (`packages/lexical-a11y/README.md`)

# `@lexical/a11y`

[](https://lexical.dev/docs/api/modules/lexical_a11y)

Framework-agnostic accessibility helpers for Lexical: ARIA live regions, focus management, roving tab index, and focus trap. Usable from React, Svelte, Vue, Solid, or vanilla DOM. React-specific wrappers around the same primitives ship from `@lexical/react`.

---

### Overview (`packages/lexical-clipboard/README.md`)

# `@lexical/clipboard`

[](https://lexical.dev/docs/api/modules/lexical_clipboard)

This package contains the functionality for the clipboard feature of Lexical.

---

### Overview (`packages/lexical-code-core/README.md`)

# `@lexical/code-core`

[](https://lexical.dev/docs/api/modules/lexical_code-core)

This package contains the base functionality for the code blocks and code highlighting for Lexical. This package
is considered an implementation detail and is being used transitionally while the deprecated highlighting
functionality is still available in `@lexical/code`.

## Caret scrolling

When Lexical moves the caret itself, it scrolls the code block sideways to show the caret. That happens on the line
start and end keys in a code block (Home and End, or Cmd+Left and Cmd+Right on macOS), on Enter, on paste and on undo.
It also happens while you type in a highlighted code block, because the highlighter retokenizes the line and Lexical
puts the caret back. Plain arrow keys and clicks move the caret natively, and the browser scrolls for them. Extending a
selection with Shift does not scroll sideways.

When the block scrolls back toward the start of the line and the caret would also fit with it scrolled all the way back,
it goes all the way back. That shows the whole indentation, which Home and Enter leave the caret after. Otherwise it
scrolls just far enough, and keeps the caret out of the code element's `scroll-padding`. A gutter that stays in place
while the code scrolls should set that padding to its width, so the caret isn't left under it. A `float: left` gutter,
like the one the playground theme draws from `data-gutter`, is on the left in both directions, so use
`scroll-padding-left` for it.

At the end of the longest line the block is scrolled as far as it goes, and a browser can draw the caret a fraction of a
pixel past that edge. A few pixels of `padding-inline-end` on the code element leave room for it. The playground theme
(`PlaygroundEditorTheme.css`) does both.

---

### Overview (`packages/lexical-code-prism/README.md`)

# `@lexical/code-prism`

[](https://lexical.dev/docs/api/modules/lexical_code-prism)

This package contains the code highlighter for Lexical using the prism engine

---

### Overview (`packages/lexical-code-shiki/README.md`)

# `@lexical/code-shiki`

[](https://lexical.dev/docs/api/modules/lexical_code-shiki)

This package contains the code highlighter for Lexical using shikijs engine.

---

### Overview (`packages/lexical-code/README.md`)

# `@lexical/code`

[](https://lexical.dev/docs/api/modules/lexical_code)

This package contains the functionality for the code blocks for Lexical. It is a
thin re-export of `@lexical/code-core` and contains no syntax highlighting.

Code highlighting lives in its own packages: depend directly on
`@lexical/code-shiki` or `@lexical/code-prism` for that. The deprecated prism
re-exports that this package used to forward from `@lexical/code-prism` have
been removed; import them from `@lexical/code-prism` instead.

---

### Overview (`packages/lexical-compiler/README.md`)

# `@lexical/compiler`

Lexical's build-time compiler: source-to-source passes over Lexical code,
delivered as a Vite/Rollup plugin and as plain transform functions for
builds with no plugin API of their own.

The **tree-shaking** pass inserts `/* @__PURE__ */`
annotations before module-scope calls to the side-effect-free factories
(`defineExtension`, `createCommand`, `createState`, `safeCast`,
`defineImportRule`, …) so bundlers can drop the extension, command, and rule
definitions an application never uses; it replaces calls to the trivial ones
with the value they would have returned; and with `strict` it refuses to let
an unannotated call hide inside a definition, where it would pin the whole
thing into the bundle.

Lexical's published `dist` bundles already carry the annotations — this
package is for builds that compile Lexical from its TypeScript source:

- consumers that opt into the `source` export condition
  (`resolve.conditions: ['source', ...]`),
- a vendored copy or a git checkout of the monorepo,
- and your own application code, whose `defineExtension`/`createCommand`
  definitions get annotated the same way.

## Why the annotations are needed

The factories are annotated with `@__NO_SIDE_EFFECTS__` where they are
defined, but esbuild only honors that for calls in the same file as the
definition, and webpack/terser do not honor it at all. Bundlers do honor a
`/* @__PURE__ */` annotation at the call site, and that is what this
transform adds.

Argument-position calls matter too: a pure call is only removable when its
arguments are also side-effect-free, so an unannotated nested `safeCast(...)`
inside a `defineExtension({...})` config pins the whole definition. The
transform annotates every module-scope call, nested ones included.

Calls inside function bodies, class fields, and static blocks are left alone —
they are not evaluated when the module is initialized, so an annotation there
has no effect on tree-shaking.

Factory calls are not the only module-scope statements a bundler has to keep.
esbuild and webpack keep, as a side effect, any property read
(`navigator.userAgent`, `Date.now`, `SomeExtension.name`), any call or `new`
they were not told is pure (`new RegExp(...)`, `Object.freeze(...)`,
`[...].join(',')`), an array or object spread, an `in` test, a `try`
statement, a mutation (`table.push(...)`, `Klass.static = ...`), and a class
with a computed member — and everything such a statement references, which is
how one `LexicalEditor.version = ...` after the class kept nearly all of
`lexical` in a bundle that imported only `createCommand`. The fix is the same
mechanism: move the work into a function declared `@__NO_SIDE_EFFECTS__` and
call it at module scope, so that this transform annotates the call and a
bundler can drop it when its result is unused. A call to a builtin or to a
third-party function that cannot be declared is annotated by hand at the call
site.

## Guaranteeing a definition can be dropped

An annotated definition is only droppable when everything it is built from is
too, so a single unannotated call in its arguments quietly pins it into every
bundle that imports the module. With `strict: true` that is a build error
naming the call rather than something you find later in a bundle analyzer:

```
@lexical/compiler: 1 call(s) evaluated inside a definition in
src/MdastFootnoteExtension.ts are not known to be side-effect free, so the
definition cannot be tree-shaken:
  1145:25 gfmFootnoteFromMarkdown(...) inside defineExtension(...)
Make the call lazy, or declare the function side-effect free
(@__NO_SIDE_EFFECTS__ plus the transform's `functions`/`namespaces` options)
so that its calls are annotated too.
```

Calls that only run later (a `nodes: () => [...]` callback, a `register`
method body) are not counted, and neither are the built-ins every bundler
already knows are pure (`new Map(...)`, `new Set(...)`, `Object`, `Symbol`,
…). Modules under `node_modules` are annotated as usual but never
checked: a definition a dependency shipped is not yours to fix. Lexical's own
build runs with `strict` on.

## Usage

Each pass is its own entry point, so a build that wants one of them does not
load the rest: `@lexical/compiler/PureAnnotations` is the tree-shaking pass,
and `@lexical/compiler` re-exports every pass for convenience.

### Vite

```js
// vite.config.js
import {pureAnnotations} from '@lexical/compiler/PureAnnotations';
import {defineConfig} from 'vite';

export default defineConfig({
  plugins: [pureAnnotations()],
  resolve: {conditions: ['source']},
});
```

The plugin defaults to `enforce: 'post'` so it runs after Vite has compiled
TypeScript and JSX away.

### Rollup

```js
// rollup.config.js
import {pureAnnotations} from '@lexical/compiler/PureAnnotations';

export default {
  plugins: [
    // ... any TypeScript/JSX plugin first
    pureAnnotations(),
  ],
};
```

### Any other bundler

`transformPureAnnotations` is the transform itself, with no bundler
integration, so it can be wrapped in a webpack loader or run as a codemod:

```js
import {transformPureAnnotations} from '@lexical/compiler/PureAnnotations';

const result = transformPureAnnotations(code, {filename});
// `null` when the module needs no annotations
const annotated = result === null ? code : result.code;
```

## Which calls get annotated

A call is annotated only when the transform can establish that the function
being called really is side-effect free. It resolves the callee to its binding
in the module and annotates the call when that binding is:

- **imported from a Lexical package** — a specifier matching `lexical` or
  `@lexical/*` (configurable with `sources`). Every factory in
  `PURE_FACTORY_FUNCTIONS` is declared `@__NO_SIDE_EFFECTS__` in those
  packages, so the import is evidence enough. Aliased imports
  (`import {defineExtension as define}`) are resolved too, and so is a
  default import from a Lexical module named for its export
  (`import warnOnlyOnce from '@lexical/internal/warnOnlyOnce'`);
- **declared in the same module with `@__NO_SIDE_EFFECTS__`**, or **imported
  from a relative module that declares it that way** — the imported file is
  read and parsed to check (turn this off with `relativeImports: false`).
  Such a declaration is evidence on its own, so a factory of your own does
  not have to be named in `functions`: mark it and its module-scope calls are
  annotated; or
- **a method of a pure namespace** — an object whose methods build values and
  touch nothing else, like `@lexical/html`'s `sel`, so that
  `sel.tag('span').attr('data-x', true)` is annotated the way a factory call
  is. The names are `PURE_NAMESPACES` (configurable with `namespaces`) when
  imported from a Lexical package, and any object marked
  `@lexical-pure-namespace` where it is declared, including through a
  relative import or a local alias. Only the outermost call of a chain is
  annotated — rollup, terser and esbuild all drop the whole chain from that
  one.

Anything else is left alone. Your own `safeCast` from `./utils`, or a
`createCommand` from some other library, will not be annotated just because it
shares a name with a Lexical factory — annotating a call that does have side
effects would let a bundler drop it. Mark your own factories
`@__NO_SIDE_EFFECTS__` (and add them to `functions`) to opt them in.

## Eliding the trivial factories

Some of the factories are pure type-level helpers whose implementation is a
trivial expression over their own arguments:

| Factory | Returns | Form |
| --- | --- | --- |
| `safeCast(value)` | `value` | `identity` |
| `defineExtension(extension)` | `extension` | `identity` |
| `defineImportRule(rule)` | `rule` | `identity` |
| `configExtension(...args)` | `args` | `args` |
| `declarePeerDependency(...args)` | `args` | `args` |

With `inline: true` the call is replaced by that expression instead of being
annotated:

```js
export const MarkExtension = defineExtension({name: '@lexical/mark', ...});
// becomes
export const MarkExtension = {name: '@lexical/mark', ...};
```

which is better than an annotation in every way that matters: a literal is
inert for every bundler with nothing to preserve through minification, there
is no nested call left to pin the definition, and the factory itself can be
dropped from the bundle once nothing calls it (the import it came from is
removed along with the last call to it).

A call that does not fit its form is annotated as usual: a spread argument
where a single parameter is expected, or a call whose result is discarded.
The replacement is parenthesized wherever bare syntax would not bind the way
the call did — `safeCast(1 + 2) * 3` is not `1 + 2 * 3`, an object literal at
the start of an expression statement would read as a block, and an `args`
array is always wrapped, since a leading `[` is a member access on whatever
the previous line ended with. Minifiers drop the parentheses again.

`createCommand` is deliberately not inlined: `{type: x}` is more bytes than a
call to a minified one-character name.

Each inlined factory is marked `@lexical-inline <form>` where it is defined.
For an import from a Lexical package the marker is documentation — the table
is trusted, because this package ships alongside that Lexical. Everywhere
else (a factory of your own, declared in the module or imported relatively)
the marker is what makes inlining possible at all: `@__NO_SIDE_EFFECTS__`
says a call is safe to *drop*, but only the marker says what the call can be
*replaced with*, so a same-named look-alike is annotated instead. Add your
factory's name to `functions`, mark it with the form its body takes, and its
calls are inlined like Lexical's own.

A marker that names a form the transform does not implement (a typo, or a
form from an older version) throws, naming the file and the forms it accepts.
Failing is the point: a marker that silently did nothing would leave the
factory un-inlined with no sign of it.

This is **off by default** because it reproduces those function bodies, so it
assumes the Lexical you are building matches this package's version — they
are released together with the same version number. Lexical's own build turns
it on for the bundles it publishes. The package's tests check that the
markers in the tree and the table agree, and that the real functions still
return what the table claims.

## Options

| Option | Default | Description |
| --- | --- | --- |
| `inline` | `false` | Replace calls to the trivial factories with the literal they would have returned (see above). |
| `functions` | `PURE_FACTORY_FUNCTIONS` | Names of the factories whose module-scope calls are annotated. Pass your own list (or `[...PURE_FACTORY_FUNCTIONS, 'myFactory']`) to cover factories of your own. |
| `namespaces` | `PURE_NAMESPACES` | Names of objects whose method calls are annotated (see above). |
| `sources` | `[/^lexical$/, /^@lexical\//]` | `RegExp` (or array) of module specifiers whose exports are trusted to be the factories without reading them. |
| `strict` | `false` | Throw when a call evaluated inside one of the definitions is not known to be side-effect free (see above). |
| `relativeImports` | `true` | Whether to read relatively imported modules to look for a `@__NO_SIDE_EFFECTS__` declaration. |
| `include` | every `.js`/`.jsx`/`.ts`/`.tsx`/`.mjs`/`.cjs`/`.mts`/`.cts` module | `RegExp` (or array) matched against the module id with any query string removed. |
| `exclude` | none | `RegExp` (or array) of module ids to skip. |
| `enforce` | `'post'` | Vite plugin ordering. |
| `parserPlugins` | none | Extra [`@babel/parser`](https://babeljs.io/docs/babel-parser#plugins) plugins, for syntax the defaults do not cover. |
| `sourceMap` | `true` | Set to `false` to skip source map generation. |

Modules that fail to parse are passed through unchanged with a warning rather
than failing the build.

The transform is idempotent: a call that already has a `/* @__PURE__ */` (or
terser's `/* #__PURE__ */`) annotation immediately before it is left alone.

## Subpath imports without tree-shaking

`subpathImports` redirects named imports and re-exports from
`@lexical/extension` to their public subpaths. For example,
`import {namedSignals, defineExtension} from '@lexical/extension'` becomes
imports from `@lexical/extension/namedSignals` and `lexical`. It reads the
installed version's barrel, including aliases, rather than using a fixed
list of symbols.

```ts
import {subpathImports} from '@lexical/compiler/SubpathImports';

export default {
  plugins: [subpathImports()],
};
```

Use this Rollup/Vite plugin before dependency resolution. It accepts
TypeScript, TSX and JavaScript, including CommonJS dependencies, standard
decorators and legacy TypeScript parameter decorators, so it can run before
or after TypeScript transpilation. Unlike `pureAnnotations`, it also helps
builds that do not tree-shake: unused extension modules never become
dependencies in the first place. It does not make an individual subpath
smaller than that subpath's own dependency graph.

Options:

- `packages`: package names, or paths to their `package.json` files. Defaults
  to `['@lexical/extension']`. Each package must publish its source files via
  the `source` export condition and declare `sideEffects: false`. Only packages
  with multiple public entries and transparent re-export barrels are narrowed;
  executable root entries are left intact.
- `root`: directory from which package names are resolved. Defaults to
  `process.cwd()`. Use absolute package.json paths for an unbuilt checkout.
- `parserPlugins`: extra `@babel/parser` plugins, appended to the defaults
  for each filename. Accepts plugin names and `[name, options]` tuples, just
  like `pureAnnotations`. Applies to consumers and package source files read
  to derive the export mapping. For example, `parserPlugins: ['doExpressions']`
  enables parsing that syntax; your downstream compiler must still transform it.
  `flow` and `flowComments` are omitted for TypeScript filenames, so Flow
  consumers can import TypeScript packages without enabling both languages
  for the same file. This also applies to `pureAnnotations`.
- `strict`: defaults to `false`. Set to `true` to reject unmappable default
  imports, namespace imports, side-effect imports, star re-exports, dynamic
  imports, TypeScript import assignments and `require` calls
  targeting a barrel. These forms load the whole namespace and cannot be
  narrowed safely. With the default setting they keep their original
  behavior. An explicitly re-exported default is narrowed like a named export.
  Checks include access inside exported declarations. Type-only imports are
  preserved and erased by TypeScript. String literals and template literals
  without substitutions are checked; computed module names cannot be narrowed.

In Rollup watch builds and Vite development servers, changes to the package export
map or barrel bindings refresh the mapping and invalidate cached consumer
transforms. Vite also includes those consumers in the hot update even when
rewriting removed the barrel from their dependency graph.

Relative imports between public entry points are also converted to package
subpaths. Lexical's package build keeps those subpaths external, so a shared
module such as `signals` has one instance across entries. The plugin does
not change a bundler's external configuration: a downstream application can
bundle the subpaths together normally.

Lexical runs this pass in strict mode for all its published package builds,
including www. Consumers can continue to import the complete barrel API
without using any compiler plugin. Barrel and subpath exports reference the
same functions, classes, extensions and signals runtime.

In this repository, source code imports from the `@lexical/extension` barrel
across package boundaries and uses relative imports within that package,
including the barrel's own re-exports. ESLint enforces this convention;
the compiler handles the published subpath layout. Direct subpath imports
remain supported for downstream consumers, including applications that need
small bundles without tree-shaking or the compiler plugin.

## SchemaJsonCodegen

The package's second entry point, `@lexical/compiler/SchemaJsonCodegen`, is
not part of the plugin pipeline: it is the library a code generator uses to
turn a node's declarative serialization schema (the `json` property of
`$config`) into straight-line JavaScript. Lexical's own
`scripts/generate-node-json.mjs` uses it to generate the specialized
`exportJSON`/`updateFromJSON` implementations the built-in nodes ship with.

- `compileParse(meta, defaultValue, tableBaseName)` compiles a schema's
  introspectable `meta` into a JavaScript expression over `v` that parses
  exactly as the schema does, plus any lookup tables the expression refers
  to. Only the kinds whose meta fully determines the parse are compiled
  (strings, numbers, booleans, enums, and `aliasedValue` tables); the rest
  throw `NotCompilable` rather than emit a guess.
- Compiling is not trusting: `verifyCompiledParse` runs the compiled
  expression against the real schema over `verificationCorpus(meta)` — every
  value the schema names plus a fixed set of hostile inputs,
  `Object.prototype` member names included — and throws naming the first
  value they disagree on. `verifyTableCoversDomain` proves an emitted lookup
  table total over a schema's domain, so its miss-fallback is dead code.
- The compact form's comparisons are compiled the same way.
  `compileDiffersFromDefault(schema, name)` states the test that a value is
  not the schema's default — a literal for a primitive default, and the
  length test `arrayValue`'s equality reduces to for an empty-array default —
  and `verifyDiffersFromDefault` runs it against the schema's own `isEqual`
  over the corpus.
- `NUM_HELPER_SOURCE` and `JSON_NUMBER_SOURCE` are the number-parsing helper
  as source text, so an emitted module and the verification share one
  definition rather than two copies that could drift; `NUM_RANGE_HELPER_SOURCE`
  is the constrained-domain variant.

The corpus is fixed rather than sampled, so a generator built on this
produces byte-reproducible output — which is what lets generated files be
checked in and drift-tested against regeneration.

---

### Overview (`packages/lexical-devtools-core/README.md`)

# `@lexical/devtools-core`

[](https://lexical.dev/docs/api/modules/lexical_devtools-core)

This package contains tools necessary to debug and develop Lexical.

---

### Overview (`packages/lexical-devtools/README.md`)

# Lexical DevTools browser extension

This is the source code for the Lexical DevTools browser extension.

[link-chrome]: https://chromewebstore.google.com/detail/lexical-developer-tools/kgljmdocanfjckcgfpcpdoklodllfdpc 'Version published on Chrome Web Store'
[link-firefox]: https://addons.mozilla.org/en-US/firefox/addon/lexical-developer-tools/ 'Version published on Mozilla Add-ons'
[link-safari]: https://apps.apple.com/us/app/lexical-developer-tools/id6502753400 'Version published on Mac App Store'

[<img src="https://cdnjs.cloudflare.com/ajax/libs/browser-logos/74.1.0/chrome/chrome.svg" width="48" alt="Chrome logo" valign="middle">][link-chrome] [<img valign="middle" src="https://img.shields.io/chrome-web-store/v/kgljmdocanfjckcgfpcpdoklodllfdpc?style=flat&label=%20">][link-chrome]

[<img src="https://cdnjs.cloudflare.com/ajax/libs/browser-logos/74.1.0/firefox/firefox.svg" width="48" alt="Firefox logo" valign="middle">][link-firefox] [<img valign="middle" src="https://img.shields.io/amo/v/lexical-developer-tools.svg?label=%20">][link-firefox]

[<img src="https://cdnjs.cloudflare.com/ajax/libs/browser-logos/74.1.0/safari/safari.svg" width="48" alt="Safari logo" valign="middle">][link-safari] [<img valign="middle" src="https://img.shields.io/itunes/v/6502753400?label=%20">][link-safari]

## Local development

Lexical DevTools extension uses [WXT](https://wxt.dev/) framework to simplify development. Please refer to [WXT Development Guide](https://wxt.dev/guide/development.html) for comprehensive documentation.

**TLDR:**
```bash
$ pnpm run dev
# In browser: Alt+R to force reload extension
```

**Useful Hints:**
- Extension activity log: [chrome://extensions/?activity=eddfjidloofnnmloonifcjkpmfmlblab](chrome://extensions/?activity=eddfjidloofnnmloonifcjkpmfmlblab)
- Status of ServiceWorkers: [chrome://serviceworker-internals/?devtools](chrome://serviceworker-internals/?devtools)
- WXT Framework debugging: `DEBUG_WXT=1 pnpm run dev`
- If you detach the Dev Tools in a separate window, and press `Cmd+Option+I` while Dev Tools window is focused, you will invoke the Dev Tools for the Dev Tools window.

**Safari:**

To develop and run Safari version of the extension you (obviously) need a Mac and Xcode installed. Safari on the contrary to other browsers doesn't accept web extensions as a zip archive but rather requires you to [wrap it in native code (Swift) wrapper](https://developer.apple.com/documentation/safariservices/safari_web_extensions/converting_a_web_extension_for_safari/). Fortunately this process is mostly automated here.

```bash
# Install Xcode

# Environment setup
sudo xcode-select -s /Applications/Xcode.app
xcodebuild --install
sudo xcodebuild -license
xcodebuild -runFirstLaunch

# Normal operation
pnpm run dev:safari

# Build & upload to Apple Connect
BUILD_VERSION=0 pnpm run safari:archive 
PASSWORD="XXX" pnpm run safari:upload
```

## Publishing

Production (`latest`) npm releases submit Chrome and Firefox extensions for review from the released commit, with build version `0`. Update store screenshots/videos before releases that change the UI.

For extension-only releases or retries, run [Publish DevTools extension to stores](https://github.com/facebook/lexical/actions/workflows/devtools-extension-publish.yml) on the release tag. Increase "Build version" for repeated uploads of the same Lexical version (`<version>.<build_version>`), and wait for pending Chrome reviews to finish. Store submissions can fail independently; retry this workflow without republishing npm packages.

Safari releases use the [manual archive/upload commands](#local-development).

### Store credentials

Enable the Chrome Web Store API in Google Cloud and [link a publishing service account](https://developer.chrome.com/docs/webstore/service-accounts) to the listing's publisher. Configure these repository settings:

| Setting | Type | Value |
| --- | --- | --- |
| `EXTENSION_CHROME_EXTENSION_ID` | Secret | Listing's extension ID |
| `EXTENSION_CHROME_PUBLISHER_ID` | Variable | **Publisher > Settings** in the [Developer Dashboard](https://chrome.google.com/webstore/devconsole) |
| `EXTENSION_CHROME_SERVICE_ACCOUNT_CLIENT_EMAIL` | Variable | Service account's `client_email` |
| `EXTENSION_CHROME_SERVICE_ACCOUNT_PRIVATE_KEY` | Secret | Decoded `private_key` PEM with actual newlines, not the full JSON key |

Firefox requires repository secrets `EXTENSION_FIREFOX_EXTENSION_ID`, `EXTENSION_FIREFOX_JWT_ISSUER`, and `EXTENSION_FIREFOX_JWT_SECRET`.

Confirm Chrome visibility and rollout in the Developer Dashboard. Visibility changes require one manual publish before API publishing. `wxt submit --dry-run` contacts Google.

### Marketplace access

Contact [Vladlen Fedosov](mailto:vladlen_fedosov@epam.com) or [Christopher Howard](mailto:christopher_howard@epam.com) at [EPAM Open Source Office](https://www.epam.com/open-source) for marketplace access. Chrome access requires a [publisher administrator](https://developer.chrome.com/docs/webstore/share-ownership). For Firefox, enable two-factor authentication, set a display name, and provide your Mozilla account email to an [existing maintainer](https://addons.mozilla.org/en-US/developers/addon/lexical-developer-tools/ownership).

## Design

This extension follows typical [Browser DevTools architecture](https://developer.chrome.com/docs/extensions/how-to/devtools/extend-devtools) that includes sereral independent contexts that communicate via events or extension APIs.

<figure align="center">
  <img src="./docs/architecture-diagram.png" alt="DevTools extension architecture" width="526">
  <figcaption>DevTools extension architecture.</figcaption>
</figure>

---

### Overview (`packages/lexical-dragon/README.md`)

# `@lexical/dragon`

[](https://lexical.dev/docs/api/modules/lexical_dragon)

This package adds compatibility with Dragon NaturallySpeaking, a speech
recognition tool whose web extension drives editable fields through window
messages. The handler applies those edits through Lexical APIs and stops the
messages from reaching the extension's own handler, which would otherwise
edit the DOM directly behind Lexical's back.

Editors register automatically through `DragonExtension` (a dependency of
`@lexical/rich-text` and `@lexical/plain-text` extensions) or by calling
`registerDragonSupport(editor)`.

## Registering early

Browsers invoke a window's message listeners in registration order, and the
Dragon extension's content script registers its listener at `document_end`
of the initial page load. Editors that mount later (after a navigation in a
single page app, in a dialog, in any lazily rendered view) register their
handler after the extension's, too late to block it, and edits end up
applied twice.

To win that race, call `installDragonSupport()` synchronously from every
entrypoint that may render an editor lazily, so the shared listener is
registered before the extension's:

```js
import {installDragonSupport} from '@lexical/dragon';

installDragonSupport();
```

It returns a teardown function; the listener is removed once every teardown
(including the ones from `registerDragonSupport`) has been called.

For editors mounted inside iframes, pass the iframe's window so the
listener is installed against the right one. State is kept per window,
so multiple frames can coexist:

```js
installDragonSupport(iframe.contentWindow);
```

---

### Overview (`packages/lexical-eslint-plugin/README.md`)

# `@lexical/eslint-plugin`

This ESLint plugin enforces the [Lexical $function convention](https://lexical.dev/docs/intro#reading-and-updating-editor-state).

**ESLint Compatibility:** This plugin supports ESLint 9 and later with the flat config format (`eslint.config.js`). Support for ESLint 7-8 and the legacy `.eslintrc` format was removed in v0.51.0.

## Installation

Assuming you already have ESLint installed, run:

```sh
npm install @lexical/eslint-plugin --save-dev
```

### Configuration

Add the recommended config to your `eslint.config.js`:

```js
import lexical from '@lexical/eslint-plugin';

export default [
  // ... other configs
  lexical.configs.recommended
];
```

`lexical.configs['flat/recommended']` and `lexical.configs['flat/all']` are
aliases of `recommended` and `all` from when those names were the legacy
configs.

### Custom Configuration

```js
import lexical from '@lexical/eslint-plugin';

export default [
  {
    plugins: {
      '@lexical': lexical
    },
    rules: {
      '@lexical/rules-of-lexical': 'error'
    }
  }
];
```

### Advanced configuration

Most of the heuristics in `@lexical/rules-of-lexical` can be extended with
additional terms or patterns.

The code example below is shown using the default implementations for each
option. When you configure these they are combined with the default
implementations using "OR", the default implementations can not be overridden.
These terms and patterns are only shown for reference and pasting this example
into your project is not useful.

If the string begins with a `"^"` or `"("` then it is treated as a RegExp,
otherwise it will be an exact match. A string may also be used instead
of an array of strings.

```js
import lexical from '@lexical/eslint-plugin';

export default [
  {
    plugins: {
      '@lexical': lexical
    },
    rules: {
      '@lexical/rules-of-lexical': [
        'error',
        {
          isDollarFunction: ['^\\$[a-z_]'],
          isIgnoredFunction: [],
          isLexicalProvider: [
            'parseEditorState',
            'read',
            'registerCommand',
            'registerNodeTransform',
            'update'
          ],
          isSafeDollarFunction: ['^\\$is']
        }
      ]
    }
  }
];
```

#### `isDollarFunction`

*Base case*: `/^\$[a-z_]/`

This defines the \$function convention, which by default is any function that
starts with a dollar sign followed by a lowercase latin letter. You may have a
secondary convention in your codebase, such as non-latin letters, or an
internal prefix that you want to consider (e.g. `"^INTERNAL_\\$"`).

#### `isIgnoredFunction`

*Base case*: None

Functions that match these patterns are ignored from analysis, they may call
Lexical \$functions but are not considered to be a dollar function themselves.

#### `isLexicalProvider`

*Base case*: `/^(parseEditorState|read|registerCommand|registerNodeTransform|update)$/`

These are functions that allow their function argument to use Lexical
\$functions.

#### `isSafeDollarFunction`

*Base case*: `/^\$is/`

These \$functions are considered safe to call from anywhere, generally
these functions are runtime type checks that do not depend on any other
state.

## Testing

To verify that the plugin works with different ESLint versions, run the integration tests:

```bash
pnpm run test-eslint-integration
```

This will test:
- ✓ ESLint 9 with a flat `eslint.config.js` configuration
- ✓ ESLint 10 with the same configuration

The tests use `pnpm dlx` to run different ESLint versions without modifying `package.json` or `pnpm-lock.yaml`.

## Valid and Invalid Examples

### Valid Examples

\$functions may be called by other \$functions

```js
function $namedCorrectly() {
  return $getRoot();
}
```

\$functions may be called in functions defined when calling the following
methods (the heuristic only considers the method name):

* `editor.update`
* `editorState.read`
* `editor.registerCommand`
* `editor.registerNodeTransform`

```js
function validUsesEditorOrState(editor) {
  editor.update(() => $getRoot());
  editor.getLatestState().read(() => $getRoot());
}
```

\$functions may be called from class methods

```js
class CustomNode extends ElementNode {
  appendText(string) {
    this.appendChild($createTextNode(string));
  }
}
```

### Invalid Examples

#### Rename autofix

```js
function invalidFunction() {
  return $getRoot();
}
function $callsInvalidFunction() {
  return invalidFunction();
}
```

*Autofix:* The function is renamed with a $ prefix. Any references to this
name in this module are also always renamed.

```js
function $invalidFunction() {
  return $getRoot();
}
function $callsInvalidFunction() {
  return $invalidFunction();
}
```

#### Rename & deprecate autofix

```js
export function exportedInvalidFunction() {
  return $getRoot();
}
```

*Autofix:* The exported function is renamed with a $ prefix. The previous name
is also exported and marked deprecated, because automatic renaming of
references to that name is limited to the module's scope.

```js
export function $exportedInvalidFunction() {
  return $getRoot();
}
/** @deprecated renamed to {@link $exportedInvalidFunction} by @lexical/eslint-plugin rules-of-lexical */
export const exportedInvalidFunction = $exportedInvalidFunction;
```

#### Rename scope conflict

```js
import {$getRoot} from 'lexical';
function InvalidComponent() {
  const [editor] = useLexicalComposerContext();
  const getRoot = useCallback(() => $getRoot(), []);
  return (<button onClick={() => editor.update(() => getRoot())} />);
}
```

*Autofix:* The function is renamed with a $ prefix and _ suffix since the suggested name was already in scope.

```js
import {$getRoot} from 'lexical';
function InvalidComponent() {
  const [editor] = useLexicalComposerContext();
  const $getRoot_ = useCallback(() => $getRoot(), []);
  return (<button onClick={() => editor.update(() => $getRoot_())} />);
}
```

---

### Overview (`packages/lexical-extension/README.md`)

# `@lexical/extension`

[](https://lexical.dev/docs/api/modules/lexical_extension)

This package provides the extension functionality for Lexical.

---

### Overview (`packages/lexical-fast-check/README.md`)

# `@lexical/fast-check`

[](https://lexical.dev/docs/api/modules/lexical_fast-check)

This package derives [fast-check](https://fast-check.dev) arbitraries from the
serialization schemas Lexical nodes declare on `$config`, so property-based tests
generate exactly the serialized JSON a node's parser accepts.

Install it alongside `fast-check`, which is a peer dependency:

```sh
npm install --save-dev @lexical/fast-check fast-check
```

## Usage

[`nodeArbitrary(NodeClass)`](https://lexical.dev/docs/api/modules/lexical_fast-check#nodearbitrary) generates the node-specific properties of a
serialized node, composing the schemas the class inherits (an element-based
node also gets `direction`/`format`/`indent`/… from `ElementNode`). Every
property is generated independently as present or absent, because that is the
domain parsers actually face: an older document predates a property, and a
compact export omits one whose value equals its default.

The values are typed as the parse shape of the class's own serialized type —
`LexicalParseJSON<SerializedMyNode>` for a `MyNode` that declares
`exportJSON(): SerializedMyNode` — which is what its `updateFromJSON` accepts,
so they can be handed to it without a cast.

```ts
import {nodeArbitrary} from '@lexical/fast-check';
import * as fc from 'fast-check';

fc.assert(
  fc.property(nodeArbitrary(MyNode), props => {
    const json = {...props, type: 'my-node'};
    // importing and re-exporting is a fixed point
    expect(exportOf(importOf(json))).toEqual(exportOf(importOf(exportOf(importOf(json)))));
  }),
);
```

[`nodeArbitrary`](https://lexical.dev/docs/api/modules/lexical_fast-check#nodearbitrary) reads the class's `$config`, so no editor or node instance is
required — but resolving it injects the class's synthesized statics and compiles
its accessor tables, so a class whose schema names an accessor it does not have
throws here. To inspect what a node declares without generating anything, call
`getComposedSchemaFields(NodeClass)` from `lexical` directly.

## Example: proving a clone carries your node's properties

A custom node has to copy its own properties in `afterCloneFrom`, which Lexical
calls whenever `getWritable()` clones the node. Declaring a new property and
forgetting to copy it there is a silent bug: the field still exists on the clone
because the constructor set it, so nothing throws — the node just reverts that
one property to its default the next time anything writes to it.

This is a good fit for a generated test, because a hand-written fixture tends to
use default values, and a dropped property compares equal to its default. The
test looks like it passed precisely when the bug is invisible. [`nodeArbitrary`](https://lexical.dev/docs/api/modules/lexical_fast-check#nodearbitrary)
draws from the schema's own domain instead, so the values are in-domain and
mostly not the default:

```
/* Detailed source-code truncated for AI context efficiency. */
```

When it fails, fast-check shrinks to the smallest value that exposes it, which
names the property for you. Deleting `this.__style = prevNode.__style` from
`TextNode.afterCloneFrom` reports:

```
Counterexample: [{"style":" "}]

-   "style": " ",
+   "style": "",
```

A single space — the smallest style that is not the default `''`, and the only
kind of value that can catch this.

---

### Overview (`packages/lexical-file/README.md`)

# `@lexical/file`

[](https://lexical.dev/docs/api/modules/lexical_file)

This package contains the functionality for the file import/export feature of Lexical.

---

### Overview (`packages/lexical-hashtag/README.md`)

# `@lexical/hashtag`

[](https://lexical.dev/docs/api/modules/lexical_hashtag)

This package contains the functionality for Lexical hashtags.

---

### Overview (`packages/lexical-headless/README.md`)

# `@lexical/headless`

[](https://lexical.dev/docs/api/modules/lexical_headless)

This package allows you to interact with Lexical in a headless environment (one that does not rely on DOM, e.g. for Node.js environment), and use its
main features like editor.update(), editor.registerNodeTransform(), editor.registerUpdateListener()
to create, update or traverse state.

Install `@lexical/headless`:

```
npm install --save @lexical/headless
```

```js
import { createHeadlessEditor } from '@lexical/headless';

const editor = createHeadlessEditor({
  nodes: [],
  onError: () => {},
});

editor.update(() => {
  $getRoot().append(
    $createParagraphNode().append(
      $createTextNode('Hello world')
    )
  )
});
```

An editor built from extensions does not need `createHeadlessEditor`: an
editor that is never given a root element already skips all DOM work. Add
`HeadlessExtension` to make that explicit. It marks the editor (and any nested
editors) as headless and makes `setRootElement` throw if it is given an
element, while extensions that register root or mutation listeners keep
working.

```js
import { buildEditorFromExtensions } from '@lexical/extension';
import { HeadlessExtension } from '@lexical/headless';
import { RichTextExtension } from '@lexical/rich-text';
import { defineExtension } from 'lexical';

const editor = buildEditorFromExtensions(
  defineExtension({
    name: '[root]',
    dependencies: [HeadlessExtension, RichTextExtension],
  }),
);
```

Lexical packages are published as ES modules. A CommonJS project can load them
with `require()` on Node.js 20.19 or later, but any dependency it shares with
Lexical (such as `yjs`) has to be loaded as an ES module as well, or the
project ends up with two copies of it; see
[Which module formats are published?](https://lexical.dev/docs/faq#which-module-formats-are-published-esm-commonjs-nodejs-react-native).

Any plugins that do not rely on DOM could also be used. Here's an example of how
you can convert lexical editor state to markdown on server:
```js
import { createHeadlessEditor } from '@lexical/headless';
import { $convertToMarkdownString, TRANSFORMERS } from '@lexical/markdown';

app.get('article/:id/markdown', async (req, res) => {
  const editor = createHeadlessEditor({
    nodes: [],
    onError: () => {},
  });

  const articleEditorStateJSON = await loadArticleBody(req.query.id);
  editor.setEditorState(editor.parseEditorState(articleEditorStateJSON));

  editor.update(() => {
    const markdown = $convertToMarkdownString(TRANSFORMERS);
    res.send(markdown);
  });
});

```

---

### Overview (`packages/lexical-history/README.md`)

# `@lexical/history`

[](https://lexical.dev/docs/api/modules/lexical_history)

This package contains history helpers for Lexical.

### Methods

#### `registerHistory`

Registers necessary listeners to manage undo/redo history stack and related editor commands. It returns `unregister` callback that cleans up all listeners and should be called on editor unmount.

```js
function registerHistory(
  editor: LexicalEditor,
  externalHistoryState: HistoryState,
  delay: number,
): () => void
```

### Commands

History package handles `UNDO_COMMAND`, `REDO_COMMAND` and `CLEAR_HISTORY_COMMAND` commands. These commands could be used to work with history state:

```jsx
import {UNDO_COMMAND, REDO_COMMAND} from 'lexical';

<Toolbar>
  <Button onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}>Undo</Button>
  <Button onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}>Redo</Button>
</Toolbar>;
```

### Undo/redo availability

`CAN_UNDO_COMMAND` and `CAN_REDO_COMMAND` are **deprecated**. A command only reports a change, so a listener registered after the editor is initialized never sees the current value, and there is no way to read it.

Use the `canUndo` and `canRedo` signals from `HistoryExtension` instead. They are derived from the history stacks and always hold the current value:

In React, use the `useExtensionSignalValue` hook:

```jsx
import {HistoryExtension} from '@lexical/history';
import {useExtensionSignalValue} from '@lexical/react/useExtensionSignalValue';

function UndoButton() {
  const canUndo = useExtensionSignalValue(HistoryExtension, 'canUndo');
  return <Button disabled={!canUndo}>Undo</Button>;
}
```

Outside React, read the signal directly:

```js
import {getExtensionDependencyFromEditor} from '@lexical/extension';
import {HistoryExtension} from '@lexical/history';

const {output} = getExtensionDependencyFromEditor(editor, HistoryExtension);
output.canUndo.peek();
```

---

### Overview (`packages/lexical-html/README.md`)

# `@lexical/html`

[](https://lexical.dev/docs/api/modules/lexical_html)

# HTML
This package exports utility functions for converting `Lexical` -> `HTML` and `HTML` -> `Lexical`. These same functions are also used in the `lexical-clipboard` package for copy and paste.

[Full documentation can be found here.](https://lexical.dev/docs/concepts/serialization)

### Exporting
```js
// In a headless mode, you need to initialize a headless browser implementation such as JSDom.
const dom = new JSDOM();
// @ts-expect-error
global.window = dom.window;
global.document = dom.window.document;
// You may also need to polyfill DocumentFragment or navigator in certain cases.

// When converting to HTML you can pass in a selection object to narrow it
// down to a certain part of the editor's contents.
const htmlString = $generateHtmlFromNodes(editor, selection | null);
```

### Importing
First we need to parse the HTML string into a DOM instance.
```js
// In the browser you can use the native DOMParser API to parse the HTML string.
const parser = new DOMParser();
const dom = parser.parseFromString(htmlString, textHtmlMimeType);

// In a headless environment you can use a package such as JSDom to parse the HTML string.
const dom = new JSDOM(htmlString);
```
And once you have the DOM instance.
```js
const nodes = $generateNodesFromDOM(editor, dom);

// Once you have the lexical nodes you can initialize an editor instance with the parsed nodes.
const editor = createEditor({ ...config, nodes });

// Or insert them at a selection.
$insertNodes(nodes);
```

---

### Overview (`packages/lexical-internal/README.md`)

# `@lexical/internal`

> [!WARNING]
> This package contains **internal** utilities shared across the Lexical
> packages: `invariant`/`devInvariant`, `createError`, dev/prod error- and warning-message
> formatting, `warnOnlyOnce`, and the `LEXICAL_VERSION` constant. These are
> bound to the build's transforms and are inlined into every other package
> rather than shipped as a runtime dependency, so it is published only so the
> modules resolve through normal package resolution — including the `source`
> export condition used when developing against a linked checkout.
>
> It is **not** a public API. Import from `lexical` and the `@lexical/*`
> feature packages instead. Nothing here follows semver and any export may
> change or disappear without notice.

`createError('Message with %s', value)` constructs an `Error` without throwing
it, for example to pass to an editor's `onWarn` handler. It shares invariant's
literal-message and `%s` argument convention, including production error-code
extraction, and can be used inside expressions.

The transform recognizes the default import from
`@lexical/internal/createError` (or its `.js` alias), including renamed imports.
Unrelated or shadowed functions with the same name are left alone. Generated
calls use separate `createDevError` and `createProdError` runtime helpers so
bundled output can pass through the transform again.

Warnings routed through `createError`, including the update-cascade warning,
use the minified error message in production builds with extracted error codes.
Arguments such as the editor namespace are encoded as `v` parameters in the
decoder URL instead of appearing directly in the message text. Telemetry that
matches the full development warning text must account for that production form.

---

### Overview (`packages/lexical-link/README.md`)

# `@lexical/link`

[](https://lexical.dev/docs/api/modules/lexical_link)

This package contains the functionality for Lexical links.

---

### Overview (`packages/lexical-list/README.md`)

# `@lexical/list`

[](https://lexical.dev/docs/api/modules/lexical_list)

This package exposes the primitives for implementing lists in Lexical. If you're trying to implement conventional lists with React, take a look at the ListPlugin exposed
by [@lexical/react](https://lexical.dev/docs/packages/lexical-react), which wraps these primitives into a neat component that you can drop into any LexicalComposer.

The API of @lexical/list primarily consists of Lexical Nodes that encapsulate list behaviors and a set of functions that can be called to trigger typical list manipulation functionality:

## Functions

### $insertList

As the name suggests, this inserts a list of the provided type according to an algorithm that tries to determine the best way to do that based on
the current Selection. For instance, if some text is selected, $insertList may try to move it into the first item in the list. See the API documentation for more detail.

### $removeList

Attempts to remove lists inside the current selection based on a set of opinionated heuristics that implement conventional editor behaviors. For instance, it converts empty ListItemNodes into empty ParagraphNodes.

## Nodes

### ListNode

### ListItemNode

## Commands

For convenience, we provide a set of commands that can be used to connect a plugin to trigger typical list manipulation functionality:

### INSERT_UNORDERED_LIST_COMMAND

### INSERT_ORDERED_LIST_COMMAND

### INSERT_CHECK_LIST_COMMAND

### REMOVE_LIST_COMMAND

It's important to note that these commands don't have any functionality on their own. They are just for convenience and require you to register a handler for them in order to actually change the editor state when they are dispatched, as below:


```ts
// MyListPlugin.ts

editor.registerCommand(INSERT_UNORDERED_LIST_COMMAND, () => {
    $insertList(editor, 'bullet');
    return true;
}, COMMAND_PRIORITY_LOW);

// MyInsertListToolbarButton.ts

function onButtonClick(e: MouseEvent) {
    editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
}

```

## Theming

Lists can be styled using the following properties in the EditorTheme passed to the editor in the initial config (the values are classes that will be applied in the denoted contexts):

```ts
{
  list?: {
    // Applies to all lists of type "bullet"
    ul?: EditorThemeClassName;
    // Used to apply specific styling to nested levels of bullet lists
    // e.g., [ 'bullet-list-level-one', 'bullet-list-level-two' ]
    ulDepth?: Array<EditorThemeClassName>;
    // Applies to all lists of type "number"
    ol?: EditorThemeClassName;
    // Used to apply specific styling to nested levels of number lists
    // e.g., [ 'number-list-level-one', 'number-list-level-two' ]
    olDepth?: Array<EditorThemeClassName>;
    // Applies to all list items
    listitem?: EditorThemeClassName;
    // Applies to all list items with checked property set to "true"
    listitemChecked?: EditorThemeClassName;
    // Applies to all list items with checked property set to "false"
    listitemUnchecked?: EditorThemeClassName;
    // Applies only to list and list items that are not at the top level.
    nested?: {
      list?: EditorThemeClassName;
      listitem?: EditorThemeClassName;
    };
  };
}
```

---

### Overview (`packages/lexical-mark/README.md`)

# `@lexical/mark`

[](https://lexical.dev/docs/api/modules/lexical_mark)

This package contains helpers and nodes for wrapping content in marks for Lexical.

---

### Overview (`packages/lexical-markdown/README.md`)

# `@lexical/markdown`

[](https://lexical.dev/docs/api/modules/lexical_markdown)

This package contains markdown helpers for Lexical: import, export and shortcuts.

## Import and export
```js
import {
  $convertFromMarkdownString,
  $convertToMarkdownString,
  TRANSFORMERS,
} from '@lexical/markdown';

editor.update(() => {
  const markdown = $convertToMarkdownString(TRANSFORMERS);
  ...
});

editor.update(() => {
  $convertFromMarkdownString(markdown, TRANSFORMERS);
});
```

It can also be used for initializing editor's state from markdown string. Here's an example with react `<RichTextPlugin>`
```jsx
<LexicalComposer initialConfig={{
  editorState: () => $convertFromMarkdownString(markdown, TRANSFORMERS)
}}>
  <RichTextPlugin />
</LexicalComposer>
```

## Shortcuts
Can use `<MarkdownShortcutPlugin>` if using React
```jsx
import { TRANSFORMERS } from '@lexical/markdown';
import {MarkdownShortcutPlugin} from '@lexical/react/LexicalMarkdownShortcutPlugin';

<LexicalComposer>
  <MarkdownShortcutPlugin transformers={TRANSFORMERS} />
</LexicalComposer>
```

Or `registerMarkdownShortcuts` to register it manually:
```js
import {
  registerMarkdownShortcuts,
  TRANSFORMERS,
} from '@lexical/markdown';

const editor = createEditor(...);
registerMarkdownShortcuts(editor, TRANSFORMERS);
```

## Transformers
Markdown functionality relies on transformers configuration. It's an array of objects that define how certain text or nodes
are processed during import, export or while typing. `@lexical/markdown` package provides set of built-in transformers:
```js
// Element transformers
UNORDERED_LIST
CODE
HEADING
ORDERED_LIST
QUOTE

// Text format transformers
BOLD_ITALIC_STAR
BOLD_ITALIC_UNDERSCORE
BOLD_STAR
BOLD_UNDERSCORE
INLINE_CODE
ITALIC_STAR
ITALIC_UNDERSCORE
STRIKETHROUGH

// Text match transformers
LINK
```

And bundles of commonly used transformers:
- `TRANSFORMERS` - all built-in transformers
- `ELEMENT_TRANSFORMERS` - all built-in element transformers
- `MULTILINE_ELEMENT_TRANSFORMERS` - all built-in multiline element transformers
- `TEXT_FORMAT_TRANSFORMERS` - all built-in text format transformers
- `TEXT_MATCH_TRANSFORMERS` - all built-in text match transformers

Transformers are explicitly passed to markdown API allowing application-specific subset of markdown or custom transformers.

There're three types of transformers:

- **Element transformer** handles top level elements (lists, headings, quotes, tables or code blocks)
- **Text format transformer** applies text range formats defined in `TextFormatType` (bold, italic, underline, strikethrough, code, subscript and superscript)
- **Text match transformer** relies on matching leaf text node content

See `MarkdownTransformers.js` for transformer implementation examples

---

### Overview (`packages/lexical-mdast/README.md`)

# `@lexical/mdast`

[](https://lexical.dev/docs/api/modules/lexical_mdast)

> ⚠️ **Experimental:** everything in this package is marked
> `@experimental` and may change between any two Lexical releases —
> including breaking renames, signature changes, or behavior changes —
> until the API stabilizes. `@lexical/markdown` remains the supported
> default for production apps that don't want to track an experimental
> API.

An alternative to `@lexical/markdown` that is built on the
[micromark](https://github.com/micromark/micromark) /
[mdast](https://github.com/syntax-tree/mdast) ecosystem.

Where `@lexical/markdown` ships its own regular-expression based parser, this
package delegates Markdown parsing and serialization to `micromark` and
`mdast-util-*`. That means **CommonMark + GFM compliance** comes from the same
parser used by `remark`, and Markdown **shortcuts** are recognized by feeding
keystrokes back through that same parser — there is no second grammar to keep
in sync.

Like `@lexical/markdown`, the original syntax of a construct is preserved on
the Lexical nodes (the bullet character of a list, a code block's fence, and a
hard line break's style), so re-serializing produces **minimally different**
Markdown — `* a`/`+ b` bullets and `~~~` fences round-trip unchanged.

## Configured through extensions

`@lexical/mdast` is set up **exclusively** through the Lexical extension
system, modeled on `@lexical/html`'s `DOMImportExtension`. Each feature
extension ships the nodes it needs and contributes its import/export rules (and
the micromark/mdast extensions that tokenize them) to the core
`MdastExtension` registry:

CommonMark features:

| Extension | Ships | Adds |
| --- | --- | --- |
| `MdastHeadingExtension` | `HeadingNode` | ATX & setext headings |
| `MdastBlockquoteExtension` | `QuoteNode` | block quotes |
| `MdastListExtension` | `ListNode`, `ListItemNode` | ordered/unordered lists |
| `MdastCodeExtension` | `CodeNode` | fenced & indented code |
| `MdastLinkExtension` | `LinkNode` | links, `<autolinks>`, reference links |
| `MdastHorizontalRuleExtension` | `HorizontalRuleNode` | thematic breaks (`---`) |

GFM features:

| Extension | Ships | Adds |
| --- | --- | --- |
| `MdastStrikethroughExtension` | – | `~~strikethrough~~` |
| `MdastTaskListExtension` | – | task lists (`- [x] …`) |
| `MdastAutolinkLiteralExtension` | – | literal autolinks (bare `https://…` in prose) |
| `MdastTableExtension` | `TableNode`, … | tables |

Behavior and convenience bundles:

| Extension | Adds |
| --- | --- |
| `MdastCommonMarkExtension` | bundle of the six CommonMark extensions |
| `MdastGfmExtension` | bundle of the four GFM extensions |
| `MdastRichTextExtension` | bundle of heading + blockquote |
| `MdastExtension` | core registry, Markdown import and export |
| `MdastShadowRootQuoteExtension` | opt-in: blockquotes as block containers (full-fidelity nested content) |
| `MdastHtmlExtension` | opt-in: raw HTML routed through the `@lexical/html` DOM import rules; HTML-encoded export via `$exportViaDOM` / `rawHtmlBlock` |
| `MdastShortcutsExtension` | streaming keyboard shortcuts |

Everything composes granularly and degrades gracefully: an editor with only
the extensions it wants imports unsupported constructs as their content
(a table becomes its cell text), and the typing shortcuts — driven by the
same registry — only fire for constructs the editor can represent (`> `
stays literal without `MdastBlockquoteExtension`).

`MdastExtension` owns the shared configuration and exposes both import and
export. Feature extensions depend on it automatically.

## Usage

```ts
import {
  $convertFromMarkdownString,
  $convertToMarkdownString,
  MdastCommonMarkExtension,
  MdastExtension,
  MdastGfmExtension,
  MdastShortcutsExtension,
} from '@lexical/mdast';
import {buildEditorFromExtensions} from '@lexical/extension';
import {defineExtension} from 'lexical';

const editor = buildEditorFromExtensions(
  defineExtension({
    // CommonMark + GFM grammar, import + export (MdastExtension), and
    // typing shortcuts. Swap bundles for individual feature extensions
    // to trim what you don't need.
    dependencies: [
      MdastCommonMarkExtension,
      MdastGfmExtension,
      MdastExtension,
      MdastShortcutsExtension,
    ],
    name: '[root]',
  }),
);

// Import / export run inside the editor; both are `$`-functions.
editor.update(() => {
  $convertFromMarkdownString('# Hello *world*');
});
const markdown = editor.read(() => $convertToMarkdownString());
```

The same API is available from the editor as
`$getExtensionOutput(MdastExtension).$convertFromMarkdownString(...)`
and
`$getExtensionOutput(MdastExtension).$convertToMarkdownString(...)`.

`$convertSelectionToMarkdownString(selection?)` serializes only the
selected content (defaulting to the current selection): unselected
blocks and list items are skipped and partially selected text is
sliced to the selected range.

### unified / remark interop

The mdast tree itself is part of the API, so editor content can flow
through the wider [unified](https://unifiedjs.com/) ecosystem — remark
plugins, `remark-rehype` for HTML rendering, tree diffing:

```ts
import {$convertFromMdast, $convertToMdast} from '@lexical/mdast';

// Editor -> mdast tree (before serialization).
const tree = editor.read(() => $convertToMdast());
// ... run remark plugins / transform the tree ...
// mdast tree -> editor.
editor.update(() => $convertFromMdast(tree));
```

The `*FromMarkdownString` functions parse the source text themselves
(which is also what enables source-based syntax preservation, e.g.
keeping `*` vs `-` bullets); the `*FromMdast` functions take an
already-parsed tree, where no source text exists so syntax
preservation is skipped.

To convert Markdown into nodes *without* replacing the document —
e.g. to insert at the current selection —
`$generateNodesFromMarkdownString(markdown)` (and its tree-taking
sibling `$generateNodesFromMdast(tree)`) returns a detached array of
block-level nodes and leaves the document and selection untouched:

```ts
import {$generateNodesFromMarkdownString} from '@lexical/mdast';

editor.update(() => {
  const selection = $getSelection();
  if ($isRangeSelection(selection)) {
    selection.insertNodes($generateNodesFromMarkdownString('# Inserted'));
  }
});
```

### Serialization options

Document-level `mdast-util-to-markdown` options (bullet, emphasis
marker, fence, ...) can be contributed like any other configuration —
scalar options in a `toMarkdownExtensions` entry apply document-wide
and override the package defaults:

```ts
import {MdastExtension} from '@lexical/mdast';
import {configExtension} from 'lexical';

// Serialize bullets as `+` and emphasis as `_`. Per-node syntax
// recorded on import (a list's bullet, a code block's fence, ...)
// still wins for those nodes' own output.
configExtension(MdastExtension, {
  toMarkdownExtensions: [{bullet: '+', emphasis: '_'}],
});
```

### Raw HTML (`MdastHtmlExtension`)

Markdown passes raw HTML through — GitHub-style `<details>` blocks, inline
`<kbd>` runs — and by default it imports as literal text. The opt-in
`MdastHtmlExtension` routes it through the editor's `@lexical/html`
`DOMImportExtension` rules instead, so **any HTML the editor can already
import works from Markdown**, and Markdown *inside* the construct keeps
working in both directions, the way it does on GitHub:

```md
<details><summary>
The *summary* line
</summary>

The **body** blocks
</details>
```

- **Import**: raw HTML block sequences and inline tag runs are reassembled
  by tag balance (CommonMark splits blocks on blank lines), parsed with
  `DOMParser`, and dispatched through the DOM import rule registry.
  Markdown between the tags is parsed with the document's own grammar and
  substituted back in with the surrounding formatting context. Unclosed
  tags — including the `<p` / `<details` prefixes typing passes through —
  stay literal text.
- **Export**: register `$exportViaDOM` for a node type and its `exportDOM`
  becomes the single source of truth for the Markdown encoding too: the
  shell is rendered, the children channel and named slots (marked with
  `data-lexical-slot`, which is stripped from the output) are substituted
  with embedded Markdown, boolean attributes are normalized, and custom
  element tags get their own lines where CommonMark requires it to
  re-parse. For hand-written encodings, `rawHtmlBlock(...parts)` builds
  the same kind of node from a template of raw tag strings, embedded
  Markdown phrasing, and `{flow}` block runs.
- **Context states**: the Markdown pipeline runs under the same
  context-record mechanism as the `@lexical/html` import/render pipelines.
  The whole import walk runs with `ImportContextMarkdown` set (so a DOM
  rule can distinguish Markdown import from HTML paste); mdast import
  handlers read ambient state with `$getImportContextValue` and layer
  state for their subtree with `$withImportContext` around
  `ctx.importChildren` — visible to nested handlers and to DOM-rule
  sessions opened for raw HTML in that subtree, and to nothing outside
  it. On export, `RenderContextMarkdownExport` lets `exportDOM` diverge
  per destination (Markdown vs the HTML clipboard), and selection exports
  (`$convertSelectionToMarkdownString`) run under
  `RenderContextMarkdownSelection` carrying the selection, so a
  contributed to-markdown handler that appends end-of-document data can
  scope its output to a clipboard copy.

A complete HTML-encoded construct is one DOM import rule (which then also
serves HTML paste) plus one export rule:

```ts
import {$exportViaDOM, MdastHtmlExtension, MdastExtension} from '@lexical/mdast';
import {defineImportRule, DOMImportExtension, sel} from '@lexical/html';
import {configExtension, defineExtension} from 'lexical';

export const MdastCollapsibleExtension = defineExtension({
  name: 'collapsible-markdown',
  nodes: [CollapsibleNode],
  dependencies: [
    MdastHtmlExtension,
    configExtension(DOMImportExtension, {
      // sel.tag('details') -> CollapsibleNode; serves Markdown and paste.
      rules: [DetailsImportRule],
    }),
    configExtension(MdastExtension, {
      // exportDOM is the single source of truth for the encoding.
      exportRules: [{$export: $exportViaDOM, type: CollapsibleNode}],
    }),
  ],
});
```

The [mdast-editor dev example](https://github.com/facebook/lexical/tree/main/dev-examples/mdast-editor)
demonstrates the block path (a `<details><summary>` collapsible with a
named summary slot), the inline path (`<kbd>` keys), and inline HTML text
formats (`<u>`, `<mark>`, `<sub>`/`<sup>`, `style="color: …"` spans).

### Custom mappings

Because extensions are the unit of configuration, you add or override behavior
by contributing rules to `MdastExtension` from your own extension:

```ts
import {MdastExtension} from '@lexical/mdast';
import {configExtension, defineExtension} from 'lexical';

export const MyMdastExtension = defineExtension({
  name: 'my-mdast',
  nodes: [MyNode],
  dependencies: [
    configExtension(MdastExtension, {
      importRules: [{type: 'myMdastType', $import: $importMyNode}],
      exportRules: [{type: MyNode, $export: $exportMyNode}],
      micromarkExtensions: [myMicromarkExtension()],
      mdastExtensions: [myMdastExtension()],
      toMarkdownExtensions: [myToMarkdownExtension()],
    }),
  ],
});
```

Export rule `type` accepts a Lexical type string or node class: `'text'` and
`TextNode` are equivalent. Rules also apply to subclasses, so a text rule
handles `TabNode` and custom text nodes unless a more specific rule exists.
The nearest matching ancestor runs first, regardless of contribution order. For
multiple rules targeting the same type (including a mix of strings and classes),
array order determines priority; contributions merged later are prepended.
Ancestor classes do not need to be registered in the editor themselves. Classes
must have their own node type; abstract classes without one (such as `ElementNode`)
are rejected.
Rules are resolved once when the editor is built.

Handlers can delegate with `context.next()`, which converts the same node
using the next handler and returns an array of output nodes. An export chain
runs lower-priority rules for the same type before moving to its ancestors,
from nearest to farthest. When no handlers remain, it uses the generic export
fallback. A handler can return the delegated result or modify it. Formatting
such as bold and italic wraps text in child nodes, so this uppercase example
walks the returned tree, including inline code:

```ts
import {MdastExtension, type MdastNode} from '@lexical/mdast';
import {configExtension, TextNode} from 'lexical';

function uppercaseText(nodes: readonly MdastNode[]): void {
  for (const node of nodes) {
    if (node.type === 'text' || node.type === 'inlineCode') {
      node.value = node.value.toUpperCase();
    } else if ('children' in node) {
      uppercaseText(node.children);
    }
  }
}

configExtension(MdastExtension, {
  exportRules: [{
    type: TextNode,
    $export: (_node, context) => {
      const output = context.next();
      uppercaseText(output);
      return output;
    },
  }],
});
```

Plain formatted text returned by text middleware is merged into adjacent text
runs so shared and overlapping formats serialize correctly. Output containing
extra fields such as `data`, or custom structure, is preserved as-is instead
of merged. Such middleware must ensure its output serializes correctly on its
own; adjacent bold nodes, for example, can produce `**a****b**`.

Import rules use mdast type strings: mdast nodes are plain objects,
without a Lexical node class hierarchy. Their `context.next()` runs the next
handler for the same type in contribution order, eventually reaching the
generic import fallback.

| Handler result | Import | Export |
| --- | --- | --- |
| `context.next()` | Runs the next handler for the same mdast type, or the generic import fallback when none remain. | Runs the next same-type or ancestor handler, or the generic export fallback when none remain. |
| `null` | Omits the node and its children. | Uses the default export directly, without trying remaining handlers. |
| `[]` | Omits the node and its children. | Omits the node. |

---

### Overview (`packages/lexical-offset/README.md`)

# `@lexical/offset`

[](https://lexical.dev/docs/api/modules/lexical_offset)

This package contains selection offset helpers for Lexical.

---

### Overview (`packages/lexical-overflow/README.md`)

# `@lexical/overflow`

[](https://lexical.dev/docs/api/modules/lexical_overflow)

This package contains selection overflow helpers and nodes for Lexical.

---

### Overview (`packages/lexical-plain-text/README.md`)

# `@lexical/plain-text`

This package provides a starting point for Lexical users by registering listeners for a set of basic commands that cover simple text-editing behavior such as entering text, deleting characters, copy + paste, or changing the selection with arrow keys.

You can use this package as a starting point, and then add additional command listeners to customize the functionality of your editor. If you want to add rich-text features, such as headings, blockquotes, or formatted text, you may want to consider using [@lexical/rich-text](https://lexical.dev/docs/packages/lexical-rich-text) instead.

---

### Overview (`packages/lexical-react/README.md`)

# `@lexical/react`

This package provides a set of components and hooks for Lexical that allow for text editing in React applications.

## Getting started

Install `lexical` and `@lexical/react`:

```
npm install --save lexical @lexical/react
```

Below is an example of a basic plain text editor using `lexical` and `@lexical/react` ([try it yourself](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-plain-text?file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview)).

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

### Overview (`packages/lexical-rich-text/README.md`)

# `@lexical/rich-text`

This package provides a starting point for Lexical users by registering listeners for a set of basic commands that cover simple text-editing behavior such as entering text, deleting characters, copy + paste, or changing the selection with arrow keys. It also provides default behavior for rich text features, such as headings, formatted, text and blockquotes.

You can use this package as a starting point, and then add additional command listeners to customize the functionality of your editor. If you don't want or need rich text functionality, you may want to consider using [@lexical/plain-text](https://lexical.dev/docs/packages/lexical-plain-text) instead.

---

### Overview (`packages/lexical-selection/README.md`)

# `@lexical/selection`

[](https://lexical.dev/docs/api/modules/lexical_selection)

This package contains selection helpers for Lexical.

---

### Overview (`packages/lexical-table/README.md`)

# `@lexical/table`

[](https://lexical.dev/docs/api/modules/lexical_table)

This package contains the functionality for the Tables feature of Lexical.

# Lexical Table Plugin

A plugin for handling tables in Lexical.

## Installation

```bash
npm install @lexical/table @lexical/react
```

## Usage

See the [react-table example](https://github.com/facebook/lexical/tree/main/examples/react-table)
for a minimal example that uses this package and the
[TablePlugin](https://lexical.dev/docs/api/modules/lexical_react_LexicalTablePlugin)
from @lexical/react/LexicalTablePlugin

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-table?file=src/main.tsx)

## Features

### Tables
- Create and edit tables with customizable rows and columns
- Support for table headers
- Cell selection and navigation
- Copy and paste support

### Limitations

#### Nested Tables
Nested tables (tables within table cells) are not supported in the editor. The following behaviors are enforced:

1. When attempting to paste a table inside an existing table cell, the paste operation is blocked.
2. The editor actively prevents the creation of nested tables through the UI or programmatically.

Note: When pasting HTML content with nested tables, the nested content will be removed by default. Make sure to implement appropriate `importDOM` handling if you need to preserve this content in some form.

This approach allows you to:
1. Detect nested tables in the imported HTML
2. Extract their content before it gets removed
3. Preserve the content in a format that works for your use case

Choose an approach that best fits your needs:
- Flatten nested tables into a single table
- Convert nested tables to a different format (e.g., lists or paragraphs)
- Store nested content as metadata for future processing

---

### Overview (`packages/lexical-tailwind/README.md`)

# `@lexical/tailwind`

[](https://lexical.dev/docs/api/modules/lexical_tailwind)

**EXPERIMENTAL**

This package contains an extension to use Tailwind for the editor theme

---

### Overview (`packages/lexical-text/README.md`)

# `@lexical/text`

[](https://lexical.dev/docs/api/modules/lexical_text)

This package contains utilities and helpers for handling Lexical text.

---

### Overview (`packages/lexical-utils/README.md`)

# `@lexical/utils`

[](https://lexical.dev/docs/api/modules/lexical_utils)

This package contains misc utilities for Lexical.

---

### Overview (`packages/lexical-website/README.md`)

# Website

This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.

### Installation

```
$ yarn
```

### Local Development

```
$ yarn start
```

This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.

### Build

```
$ yarn build
```

This command generates static content into the `build` directory and can be served using any static contents hosting service.

### Deployment

Using SSH:

```
$ USE_SSH=true yarn deploy
```

Not using SSH:

```
$ GIT_USER=<Your GitHub username> yarn deploy
```

If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.

---

### Overview (`packages/lexical-yjs/README.md`)

# `@lexical/yjs`

[](https://lexical.dev/docs/api/modules/lexical_yjs)

This package provides a set of bindings for Y.js that allow for collaborative editing with Lexical.

---

### Overview (`packages/lexical/README.md`)

# `lexical`

[](https://lexical.dev/docs/api/modules/lexical)

Lexical is an extensible JavaScript web text-editor framework with an emphasis on reliability, accessibility, and performance. Lexical aims to provide a best-in-class developer experience, so you can easily prototype and build features with confidence. Combined with a highly extensible architecture, Lexical allows developers to create unique text editing experiences that scale in size and functionality.

The core of Lexical is a dependency-free text editor engine that allows for powerful, simple and complex,
editor implementations to be built on top. Lexical's engine provides three main parts:

- editor instances that each attach to a single content editable element.
- a set of editor states that represent the current and pending states of the editor at any given time.
- a DOM reconciler that takes a set of editor states, diffs the changes, and updates the DOM according to their state.

By design, the core of Lexical tries to be as minimal as possible.
Lexical doesn't directly concern itself with things that monolithic editors tend to do, such as UI components, toolbars, rich-text features or markdown. Instead,
those features are added to an editor as [extensions](https://lexical.dev/docs/extensions/intro) from optional packages, which are used as and when they're needed. This ensures great extensibility and keeps code sizes
to a minimum, so apps only pay the cost for what they actually import.

For React apps, the optional `@lexical/react` package provides React bindings, including `LexicalExtensionComposer`, which builds an editor from extensions and renders it.

## Usage

The `lexical` package contains the core Lexical engine and nodes, and the extension system that the other packages build on. Most applications build an editor with `buildEditorFromExtensions()` from `@lexical/extension` (or with `@lexical/react`), and add features such as rich text and history with the extensions from other `@lexical/*` packages.

## Working with Lexical

This section covers how to use Lexical independently of any framework or library. For React, see [Getting Started with React](https://lexical.dev/docs/getting-started/react).

### Creating an editor and using it

When you work with Lexical, you normally work with a single editor instance. An editor instance can be thought of as the one responsible
for wiring up an EditorState with the DOM. You build one from extensions: each extension bundles the nodes, configuration, commands and listeners for a feature, together with any extensions it depends on.

```js
import {buildEditorFromExtensions} from '@lexical/extension';
import {HistoryExtension} from '@lexical/history';
import {RichTextExtension} from '@lexical/rich-text';
import {defineExtension} from 'lexical';

const editor = buildEditorFromExtensions(
  defineExtension({
    dependencies: [RichTextExtension, HistoryExtension],
    name: '@my-app/editor',
    namespace: 'MyEditor',
    theme: {
      // ...
    },
  }),
);
```

Once you have an editor instance, when ready, you can associate the editor instance with a content editable `<div>` element in your document:

```js
const contentEditableElement = document.getElementById('editor');

editor.setRootElement(contentEditableElement);
```

If you want to clear the editor instance from the element, you can pass `null`. Alternatively, you can switch to another element if need be,
just pass an alternative element reference to `setRootElement()`. When you are done with the editor, call `editor.dispose()`.

An editor that never gets a root element does no DOM work at all, so the same code can run on a server or in tests.

### Understanding the Editor State

With Lexical, the source of truth is not the DOM, but rather an underlying state model
that Lexical maintains and associates with an editor instance. You can get the latest
editor state from an editor by calling `editor.getEditorState()`.

Editor states have two phases:

- During an update they can be thought of as "mutable". See "Updating an editor" below to
  mutate an editor state.
- After an update, the editor state is then locked and deemed immutable from there on. This
  editor state can therefore be thought of as a "snapshot".

Editor states contain two core things:

- The editor node tree (starting from the root node).
- The editor selection (which can be null).

Editor states are serializable to JSON, and the editor instance provides a useful method
to deserialize stringified editor states.

```js
const stringifiedEditorState = JSON.stringify(editor.getEditorState());

const newEditorState = editor.parseEditorState(stringifiedEditorState);
editor.setEditorState(newEditorState);
```

To start an editor with some content, give its extension an `$initialEditorState`: a stringified editor state, or a function that builds the content inside an update.

### Updating an editor

There are a few ways to update an editor instance:

- Trigger an update with `editor.update()`
- Setting the editor state via `editor.setEditorState()`
- Applying a change as part of an existing update via `editor.registerNodeTransform()`
- Using a command listener with `editor.registerCommand(EXAMPLE_COMMAND, () => {...}, priority)`

The most common way to update the editor is to use `editor.update()`. Calling this function
requires a function to be passed in that will provide access to mutate the underlying
editor state. When starting a fresh update, the current editor state is cloned and
used as the starting point. From a technical perspective, this means that Lexical leverages a technique
called double-buffering during updates. There's an editor state to represent what is current on
the screen, and another work-in-progress editor state that represents future changes.

Creating an update is typically an async process that allows Lexical to batch multiple updates together in
a single update, improving performance. When Lexical is ready to commit the update to
the DOM, the underlying mutations and changes in the update will form a new immutable
editor state. Calling `editor.getEditorState()` will then return the latest editor state
based on the changes from the update.

Here's an example of how you can update an editor instance:

```js
import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';

// Inside the `editor.update` you can use special $ prefixed helper functions.
// These functions cannot be used outside the closure, and will error if you try.
// (If you're familiar with React, you can imagine these to be a bit like using a hook
// outside of a React function component).
editor.update(() => {
  // Get the RootNode from the EditorState
  const root = $getRoot();

  // Create a new ParagraphNode
  const paragraphNode = $createParagraphNode();

  // Create a new TextNode
  const textNode = $createTextNode('Hello world');

  // Append the text node to the paragraph
  paragraphNode.append(textNode);

  // Finally, append the paragraph to the root
  root.append(paragraphNode);
});
```

To read the editor state without changing it, use `editor.read()`, which also accepts the $ prefixed helper functions:

```js
const text = editor.read(() => $getRoot().getTextContent());
```

If you want to know when the editor updates so you can react to the changes, you can add an update
listener. The usual place to register one is the `register` function of your own extension, which is
called when the editor is built and cleans up when it is disposed:

```js
import {$getRoot, defineExtension} from 'lexical';

const WordCountExtension = defineExtension({
  name: '@my-app/WordCount',
  register: (editor) =>
    editor.registerUpdateListener(({editorState}) => {
      // The latest EditorState can be found as `editorState`.
      // To read its contents, pass `{editor}` so the $ prefixed helper
      // functions can find the editor too.
      editorState.read(
        () => {
          const words = $getRoot().getTextContent().split(/\s+/);
          console.log(words.filter(Boolean).length);
        },
        {editor},
      );
    }),
});
```

Add `WordCountExtension` to your editor's `dependencies` alongside the extensions it already uses.

To learn more, see the [Lexical documentation](https://lexical.dev/docs/intro).

---

### Overview (`.github/actions/internal-registry/README.md`)

# Internal registry actions

Two composite actions that let a GitHub Actions job talk to the internal npm
registry without a static token. The job's own OIDC identity is exchanged for a
short-lived credential, per run.

| action | what it does |
| --- | --- |
| [`auth`](./auth) | Mints the credential. Optionally writes it into the npm user config so later `install` / `publish` steps just work. |
| [`dependency-check`](./dependency-check) | Asserts every version pinned in a lockfile is actually served by the registry. |

Both are versioned with their own tags (`internal-registry-auth-v1`,
`internal-registry-dependency-check-v2`). Pin a tag — do not track `main`.

## Using it from another repository

They are public actions, so any repository in any organization can use them.

Read-only check:

```yaml
jobs:
  dependency-check:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write # required, and it must be set on the JOB
    steps:
      - uses: actions/checkout@v7
      - id: auth
        uses: facebook/lexical/.github/actions/internal-registry/auth@internal-registry-auth-v1
        with:
          write-npmrc: 'false'
          on-missing-oidc: skip
      - uses: facebook/lexical/.github/actions/internal-registry/dependency-check@internal-registry-dependency-check-v2
        with:
          token: ${{ steps.auth.outputs.token }}
```

Installing or publishing scoped packages:

```yaml
      - uses: actions/setup-node@v6 # BEFORE auth: setup-node rewrites the npm config
        with:
          node-version: '24'
      - uses: facebook/lexical/.github/actions/internal-registry/auth@internal-registry-auth-v1
        with:
          scopes: '@acme @acme-ui'
      - run: npm publish
```

## Things that will bite you

- **`permissions: id-token: write` must be on the job.** A job-level
  `permissions` block *replaces* the workflow-level one rather than merging
  with it, so setting it only at the workflow level leaves the job with no
  OIDC identity.
- **Run `actions/setup-node` before `auth`, not after.** When `setup-node` is
  given a `registry-url` it rewrites the npm user config, which would discard
  what `auth` wrote.
- **`auth` writes *user* config, not a project-local `.npmrc`.** npm does not
  walk up from the directory it publishes from, so a repo-root `.npmrc` is
  invisible to `npm publish` run in a subdirectory. This is the usual cause of
  a confusing `ENEEDAUTH` when the credential is demonstrably present.
- **The two actions compose in the workflow; they do not nest.** A local
  `uses: ./...` inside a composite action resolves against the *caller's*
  workspace, not the repository the action came from, so a remote composite
  action cannot reference a sibling. This is why `dependency-check` takes a
  `token` input instead of calling `auth` itself.
- **Fork pull requests have no OIDC identity.** Use `on-missing-oidc: skip` and
  gate on a required `merge_group` run.

## Diagnosing a rejected credential

`auth` logs the OIDC subject it minted for:

```
Credential minted for subject: repo:my-org/my-repo:ref:refs/heads/main
```

That string is what the registry authorizes against, so quote it when asking
for access. It is not a secret; the credential itself is masked. Note that some
repositories are configured to emit an immutable subject that embeds numeric
IDs (`repo:my-org@123/my-repo@456:...`) — check with:

```bash
gh api repos/<owner>/<repo>/actions/oidc/customization/sub
```

---

### Overview (`packages/lexical-eslint-plugin/__tests__/integration/README.md`)

# ESLint Plugin Integration Tests

This directory contains integration tests that verify `@lexical/eslint-plugin`
works correctly with every supported ESLint version.

## Test Coverage

The integration tests verify:

- **ESLint 9.x** with a flat `eslint.config.js` configuration
- **ESLint 10.x** with the same configuration

## Running the Tests

From the repository root:

```bash
pnpm run test-eslint-integration
```

## How It Works

The test script uses `pnpm dlx` to run different ESLint versions without
modifying `package.json` or `pnpm-lock.yaml`. This ensures:

- No dependency conflicts
- Clean testing environment
- Multiple ESLint versions can be tested in the same run

## Test Fixtures

### Flat Config

Located in `fixtures/flat-config/`:

- `eslint.config.js` - Flat ESLint configuration using `configs.recommended`
- `valid.js` - Code that should pass linting
- `invalid.js` - Code that should trigger `@lexical/rules-of-lexical` errors

## Expected Behavior

### Valid Code Examples

These should **pass** linting:

- Functions with `$` prefix calling other `$` functions
- Code inside `editor.update()` callbacks calling `$` functions
- Class methods calling `$` functions

### Invalid Code Examples

These should **fail** linting with `@lexical/rules-of-lexical` error:

- Functions without `$` prefix calling `$` functions directly

## Test Output

The test script provides colored output:

- ✓ Green = Test passed
- ✗ Red = Test failed
- Summary at the end with total/passed/failed counts

---

### Overview (`packages/lexical-website/plugins/example-source/README.md`)

# Example source in documentation

Getting Started guides import runnable example code as strings and render it with Docusaurus `CodeBlock`. Edit the example source to update these blocks; do not copy the code into MDX.

Mark each fragment with a stable name and a comment identifying the guide that reads it:

```ts
// [docs:my-extension] Read directly by the Creating an Extension guide.
export const MyExtension = defineExtension({
  name: 'MyExtension',
});
// [/docs:my-extension]
```

Then import that region in the guide:

```mdx
import CodeBlock from '@theme/CodeBlock';
import MyExtensionSource from '!!example-source?region=my-extension!@site/../../examples/my-example/src/MyExtension.ts';

<CodeBlock language="ts" title="examples/my-example/src/MyExtension.ts">
  {MyExtensionSource}
</CodeBlock>
```

The local `example-source` loader is registered in `docusaurus.config.ts`. `!!` bypasses the normal source loaders so the example is displayed as text, not executed in the documentation page. The bundler tracks the imported file, so edits also refresh the documentation during development.

Use named markers, never line ranges. Formatting changes and unrelated insertions must not change which code the guide includes. Each imported region must have exactly one opening and one closing marker, in that order, and contain nonempty source. A missing, duplicated, reversed, or empty region fails the documentation build with the file path and region name.

Regions may be nested. All marker comments are removed from the displayed code, and common indentation is removed. CSS uses `/* [docs:name] */` and `/* [/docs:name] */`; HTML uses `` and ``.

The loader tests run as part of `test-examples` in CI. Small illustrative snippets that explain an alternative usage may still be written in MDX; code taken from a runnable example should use this loader.

---

### Overview (`packages/lexical-website/src/data/README.md`)

# Team Data

This directory contains dynamically generated data for the Lexical team page.

## team.json

The `team.json` file contains information about:
- **Core Team**: Active maintainers with PR review access who have contributed in the last 12 months
- **Core Team Emeriti**: Previous core team members who haven't been active recently but made significant contributions
- **Distinguished Contributors**: Community members with substantial contributions (11+ total contributions)

### Updating Team Data

The team data is automatically generated from GitHub contributor data using the `generate-team-data.js` script.

To update the team data:

```bash
pnpm run update-team-data
```

This script:
1. Fetches all contributors from the GitHub API
2. Checks recent activity (last 12 months) for tracked team members
3. Fetches user profile information (name, Twitter, company)
4. Categorizes contributors based on:
   - Whether they have PR review access
   - Recent commit activity
   - Total contribution count
5. Generates the `team.json` file with sorted team members

### Configuration

The team categorization logic is configured in `scripts/generate-team-data.mjs`

### When to Update

Update the team data:
- Occasionally to reflect recent contributions
- When adding new core team members
- When team members change roles or organizations

### Requirements

- GitHub CLI (`gh`) must be installed and authenticated
- Internet connection to access GitHub API
- Node.js 18+

---

### Overview (`packages/lexical/src/__bench__/README.md`)

# Lexical Benchmarks

Performance benchmarks for the Lexical core. Run via `pnpm bench` from the
repo root, or scoped to a single project with `--project "bench (bench)"` (data
structure microbenches, node env) or `--project "bench-dom (bench)"` (real-editor
benches, jsdom env).

```sh
pnpm bench                                       # all bench projects
pnpm vitest bench --project "bench (bench)"         # microbenches only
pnpm vitest bench --project "bench-dom (bench)"     # editor cycle benches only
pnpm vitest bench --project "bench (bench)" nodeMap # filter by file substring
```

## Two projects, why?

Microbenches that exercise pure data structures (`Map` vs `GenMap`, etc.)
run in `bench` (node env, no DOM) — fastest startup, cleanest numbers.

Benches that exercise a real Lexical editor (which needs a DOM) run in
`bench-dom` (jsdom env). Files in `__bench__/dom/**` are picked up only
by this project. The two projects share the same `pnpm bench` entry but
otherwise stay isolated to keep microbench results uncontaminated by
jsdom setup cost.

## What benches live here

| File | Project | Measures |
| ---- | ------- | -------- |
| `nodeMap.bench.ts` | `bench` | `Map` vs `GenMap` on clone / typing / paste / iteration / get |
| `getWritable.bench.ts` | `bench` | headless writes, replacement, and selection formatting at fixed document sizes |
| `dom/editorCycle.bench.ts` | `bench-dom` | real `editor.update` cycle cost on a jsdom-backed editor |
| `dom/editorOperations.bench.ts` | `bench-dom` | editor operations: split, format, delete range, paste, select-all |

Helpers shared across files live in `_utils.ts` (microbench) and
`dom/_utils.ts` (real-editor). Use them when you can; extract new helpers
there if your bench file grows beyond a single workload.

For writable-node changes, compare production bundles with the same current
benchmark on every revision:

```sh
node scripts/bench-get-writable.mjs <base-ref> [other-refs...] > results.jsonl
```

The Vitest writable-node benchmark checks both alternating edits during setup
and the final document after timing, so a no-op cannot hide behind the parity
of the last iteration.

The runner applies the shared package-build Babel options (including the
repository's Browserslist targets and production error transform) before
esbuild bundles JavaScript with `target: 'esnext'`. Native class fields are
preserved. The shared Terser settings use ES2021 and two compression passes;
the runner additionally verifies that all development constants are eliminated
before importing the bundle. Every revision uses the same current build options,
so the comparison measures source changes under that configuration, not the
isolated effect of changing build settings. This is a headless source bundle,
not the published package layout or the compiler annotation pipeline.

The runner also includes the working tree. It verifies each workload before
timing, rotates revision order across nine samples, and reports median
microseconds per update plus the individual samples. This reduces timing
drift between separate runs. Measurements cover headless updates without DOM
reconciliation; small differences still need to be treated as noise.
Set `LEXICAL_BENCH_FILTER` to a regular expression over the workload name and
`LEXICAL_BENCH_SAMPLES` to a positive integer for longer, focused comparisons.

## When to add a bench

Add a bench when you are landing a perf change and want to:

- Establish a baseline so future regressions are detectable.
- Justify a non-obvious algorithmic choice with measurements.
- Compare implementation strategies (the `nodeMap.bench.ts` workflow).

Skip a bench for changes whose perf impact is obvious or unmeasurable in
isolation.

## Conventions

**File naming**

- `*.bench.ts` directly under `__bench__/` for node-env microbenches.
- `*.bench.ts` under `__bench__/dom/` for jsdom-env benches.

**Structure** — sweep across realistic sizes; one `test` per scenario;
one `bench` per implementation under test. Vitest 5 provides `bench` through
the test context. Call `.run()` for a single benchmark or `bench.compare()`
for multiple implementations.

```ts
import {test} from 'vitest';

import {buildMap, type FakeNode} from './_utils';
import {MyImpl} from '../MyImpl';

const SIZES = [100, 1000, 10000, 100000] as const;

for (const size of SIZES) {
  test(`size=${size} :: <scenario name>`, async ({bench}) => {
    let oldImpl: Map<string, FakeNode>;
    let newImpl: MyImpl<string, FakeNode>;

    await bench.compare(
      bench(
        'old',
        {
          beforeAll: () => {
            oldImpl = buildMap(size);
          },
        },
        () => {
          // operation under test using oldImpl
        },
      ),
      bench(
        'new',
        {
          beforeAll: () => {
            newImpl = MyImpl.fromMap(buildMap(size));
          },
        },
        () => {
          // operation under test using newImpl
        },
      ),
    );
  });
}
```

**Comparison pattern** — pass both implementations to `bench.compare()` in
one test. Vitest reports their relative performance in a comparison table.

**Setup state** — use the `beforeAll` benchmark option to initialize state
before warmup and again before measured iterations. It does not run for
every iteration. Use `beforeEach` and `afterEach` benchmark options for
per-iteration work outside the timed body. Timing options such as `time`,
`iterations`, and `warmupTime` go in `.run(options)` or the final argument
to `bench.compare()`. Mutations made by the benchmark body accumulate across
iterations unless a hook resets the state.

**DCE prevention** — V8 may eliminate calls whose return values are
unused. Each bench file should declare a module-level scalar sink and
assign into it from the timed body so the call can't be elided:

```ts
let _benchSink: unknown;

test('map lookup', async ({bench}) => {
  await bench('get', () => {
    _benchSink = map.get(someKey);
  }).run();
});
```

For loops, accumulate into a local and assign the local once at the end:

```ts
test('map iteration', async ({bench}) => {
  await bench('iterate', () => {
    let count = 0;
    for (const _ of map) count++;
    _benchSink = count;
  }).run();
});
```

A scalar assignment (rather than an array push) avoids unbounded memory
growth at high iteration counts — e.g. size=100000 `get` can run millions
of iterations per bench cycle.

## Reading results

Vitest reports throughput (ops/sec), latency, percentiles, and relative
performance in each comparison table. Compare implementations within the
same table; mean latency is useful as an absolute number, while high
percentiles indicate tail behavior under GC or compaction.

Sample size and warmup are managed by Vitest. The default heuristics are
fine for stable comparisons between two impls on the same machine; for
absolute numbers you intend to publish, run multiple times and report the
median.

## Limits

- Numbers are **machine-dependent**. Compare runs on the same machine and
  the same load. Don't compare numbers across hardware.
- jsdom DOM ops are slower than real browsers. Treat `bench-dom` numbers
  as relative comparisons, not absolute production estimates.
- Benchmark timings do not run in CI. Benchmark types are checked by
  `pnpm run tsc-test`, which is part of `pnpm run ci-check`.

---

### Packages Lexical Website Docs Intro (`packages/lexical-website/docs/intro.md`)

---
sidebar_position: 1
---

# Introduction

Lexical is an extensible text editor framework for the web, built for
reliability, accessibility, and performance. It gives you a small,
dependency-free core and a set of optional packages that you compose into the
editor you need, from a plain-text input with mentions to a collaborative
rich-text document editor.

Lexical attaches to a `contenteditable` element and keeps its own model of the
document. You read and change that model with Lexical's APIs, and Lexical
takes care of keeping the DOM, the selection, and the browser's many
`contenteditable` quirks in sync. Most code never touches the DOM directly;
the main exception is custom nodes, which define how they render.

## What can you build? {#what-can-be-built-with-lexical}

- Plain-text inputs that need more than a `<textarea>`: mentions, hashtags,
  links, custom emoji.
- Rich-text editors for comments, posts, and messages.
- Full document editors with tables, lists, code blocks, and images, for a CMS
  or a notes app.
- Real-time collaborative editing with shared content and remote cursors,
  using the [Yjs integration](./collaboration/react.md).

Lexical supplies the editing infrastructure. Your application supplies the
layout, toolbars, menus, styling, and storage.

Lexical is an open-source community project. It began at Meta, and its
contributors today include Meta engineers alongside many volunteers and
developers from other companies. It powers text editing in Meta's web products
and at [Bloomberg](https://www.bloomberg.com), and is also the editor behind
[Payload CMS](https://payloadcms.com),
[Supabase](https://supabase.com),
[Proton Docs](https://proton.me/drive/document-editor),
[MDXEditor](https://mdxeditor.dev), [Sveltia CMS](https://sveltiacms.app),
[Dify](https://dify.ai), [RAGFlow](https://ragflow.io),
[DeepSeek Harness](https://deepseek.com/harness/),
[Paperclip](https://paperclip.ing), and [Lexxy](https://lexxy.dev). To see
what it can do, try the [playground](https://playground.lexical.dev).

## How it fits together

The `lexical` package is the core: the editor, the editor state, the base
node types, selection, commands, and the DOM reconciler. Everything else is an
optional package built on top of it, so an application only includes the
features it uses. The core is framework-agnostic, and `@lexical/react`
provides React bindings.

```mermaid
flowchart TB
  accTitle: How Lexical's packages fit together
  accDescr: Your application uses optional packages such as rich text, history, lists, tables, and React bindings. Both the application and those packages build on the lexical core, which contains the editor, the editor state, commands, and the DOM reconciler.
  app["Your application"]
  subgraph features["Optional packages"]
    rich["@lexical/rich-text<br/>@lexical/plain-text"] ~~~ more["@lexical/history<br/>@lexical/list<br/>@lexical/table<br/>…"] ~~~ bindings["@lexical/react"]
  end
  subgraph core["lexical (core)"]
    editor["Editor"] ~~~ state["EditorState<br/>(node tree + selection)"] ~~~ commands["Commands"] ~~~ reconciler["DOM reconciler"]
  end
  app --> features
  app --> core
  features --> core
```

Features are added to an editor as [extensions](./extensions/intro.md). An
extension bundles everything a feature needs (its nodes, configuration,
commands, and listeners, plus any extensions it depends on) so it can be
added in one place:

```ts
import {buildEditorFromExtensions} from '@lexical/extension';
import {HistoryExtension} from '@lexical/history';
import {RichTextExtension} from '@lexical/rich-text';
import {defineExtension} from 'lexical';

const editor = buildEditorFromExtensions(
  defineExtension({
    dependencies: [RichTextExtension, HistoryExtension],
    name: '@my-app/editor',
    namespace: 'my-app',
  }),
);
editor.setRootElement(document.getElementById('editor'));
```

In React, [`LexicalExtensionComposer`](./extensions/react.md) does the same
job. The [Quick Start](/docs/getting-started/quick-start) and
[React guide](/docs/getting-started/react) walk through a complete setup.

## Core concepts {#lexicals-design}

These are the ideas the rest of the documentation builds on. Each one links to
a page with the details.

### Editor {#editor-instances}

The editor wires everything together. It owns the current editor state,
attaches to a root DOM element, and is where you register nodes, listeners,
transforms, and commands. You usually create it with
`buildEditorFromExtensions` or through the React bindings rather than calling
`createEditor()` yourself.

### Editor state {#editor-states}

An [editor state](./concepts/editor-state.md) is an immutable snapshot of the
document: a tree of [nodes](./concepts/nodes.mdx) under a single `RootNode`,
plus a [selection](./concepts/selection.md). The editor state, not the DOM, is
the source of truth. Its `toJSON()` output is how you save and restore content.

### A DOM-like document tree {#document-model}

Lexical's node tree is shaped like the HTML it renders. A paragraph contains
its text, and a link is an element that contains the text it wraps. See
[Document Model](./concepts/document-model.md), and
[Compared with ProseMirror](./concepts/compared-with-prosemirror.md) for how
this differs from a mark-based editor.

### Reading and updating editor state {#reading-and-updating-editor-state}

All reads and writes happen inside a synchronous callback, such as
`editor.update(fn)` or `editor.read('force-commit', fn)`. Functions whose
names start with `$`, such as `$getRoot()`, only work inside those callbacks,
in the same way React Hooks only work while a component renders. See
[Editor State](./concepts/editor-state.md#updating-state).

### Updates and the DOM reconciler {#dom-reconciler}

Updates change a pending copy of the editor state. Updates in the same tick
are batched, and when the batch commits, the reconciler patches only the DOM
that belongs to changed nodes. Lexical also watches for DOM changes made
outside it and keeps or reverts them. [Updates](./concepts/updates.mdx) walks
through each phase, with an interactive example.

### Commands, transforms, and listeners {#listeners-node-transforms-and-commands}

Most editor behavior is built from three kinds of hooks.
[Commands](./concepts/commands.md) carry user input and your own actions to
handlers in priority order. [Node transforms](./concepts/transforms.md) run
during an update to keep the document in shape.
[Listeners](./concepts/listeners.md) react after an update is committed.

### Serialization and running on a server

Content converts to and from JSON, HTML, and Markdown; see
[Serialization](./serialization/serialization.md). An editor with no root
element does no DOM work at all, so the same code can run on a server or in
tests; see [Running Without a Browser](./concepts/headless.md).

## Get started

- [Quick Start](/docs/getting-started/quick-start) builds an editor without a
  framework, and [Getting Started with React](/docs/getting-started/react) does
  the same in React.
- [Lexical Extensions](./extensions/intro.md) and
  [Included Extensions](./extensions/included-extensions.md) cover how to add
  features.
- The [playground](https://playground.lexical.dev) shows many features working
  together.

---

### Packages Lexical Website Docs Extensions Intro (`packages/lexical-website/docs/extensions/intro.md`)

# Lexical Extensions

Extensions are a new convention to add configuration and behavior to a Lexical
editor in a modular and framework-independent manner. It is easier to use and
more powerful than the React plug-in scheme that came before, and is framework
agnostic.

See [Included Extensions](./included-extensions) for the list of extensions
that come with Lexical and its associated packages.

## Use Case

Lexical Extensions allow complex features to be added to your editor
in a single place. If you want to have an editor that supports check
lists, you simply add the `CheckListExtension` to your dependencies.
All of its dependent configuration, registration, and dependencies on
other extensions is encapsulated. Extensions also work the same way
with or without React, so you don't generally need separate
documentation and exports to support both.

All of the complexity for how and when things need to happen is
moved to `buildEditorFromExtensions` and the definitions of the
extensions themselves.

In contrast, using checklists without Lexical Extensions requires
`ListNode` and `ListItemNode` to be configured, and both the `ListPlugin` and
`CheckListPlugin` to be registered exactly one time each (or if not using
React: `registerList` and `registerCheckList`).

Customizing a Lexical editor without Lexical Extensions is done in two phases:

* Configuration before the editor is constructed
  (registered nodes, node replacement, theme, etc.)
* Registration after the editor is constructed
  (listeners, transforms, commands, react plug-ins, etc.)

In many cases, adding a feature to the editor requires *both* configuration
and registration. These typically happen at very different places in the code,
possibly even different files, and there are no transitive dependencies. It's
up to you to make sure that every required snippet of configuration and
registration code is included exactly once in the right place. When some dependent
configuration or registration is missing (or runs twice!), at best you get a runtime
error, but often it will partially work which is tricky to debug.

Another compelling use case for extensions is to provide functionality
from one extension to another extension (e.g. collaboration with yjs),
or have one extension enhance the capabilities of another (e.g. markdown
import/export, adding options to menus or toolbars, etc.).
This is very awkward and error-prone with legacy plug-ins since there is no
established pattern for them to depend on each other, and providing this
sort of functionality with React requires a strict hierarchy of context
providers. Any extension can provide output, which is equivalent to having
extension-specific context scoped to an editor.

## Core API Overview

The primary use case for extensions is to build an editor from a collection of
extensions. Any configuration or behavior specific to your editor should be
provided as an extension with
[defineExtension](/docs/api/modules/lexical#defineextension) or as an override
of some dependency's configuration with
[configExtension](/docs/api/modules/lexical#configextension). This is done
with either:

* [buildEditorFromExtensions](/docs/api/modules/lexical_extension#buildeditorfromextensions) - when not using React
* [LexicalExtensionComposer](/docs/api/modules/lexical_react_LexicalExtensionComposer) - when using React

See [Defining Extensions](./defining-extensions.md) for a detailed guide for
`defineExtension`.

The following functions are included in the core
[lexical](/docs/api/modules/lexical) module to ensure that defining
extensions is lightweight and requires no dependency on the runtime
features of [@lexical/extension](/docs/api/modules/lexical_extension).
These functions are primarily for type inference with very minimal
implementations.

* [defineExtension](/docs/api/modules/lexical#defineextension) - define an extension
* [configExtension](/docs/api/modules/lexical#configextension) - provide configuration overrides for an extension
* [declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency) - declare an indirect dependency on another extension by name
* [safeCast](/docs/api/modules/lexical#safecast) - a convenience for TypeScript casting, with more safety than `as`. Similar to using a variable with a declared type.
* [shallowMergeConfig](/docs/api/modules/lexical#shallowmergeconfig) - the default configuration merge strategy

## Advanced features

- [Signals](./signals) - Can be used for extensions to change their configuration or behavior at runtime, such as dynamically enabling or disabling an extension. This is also used to facilitate some performance optimizations around subscriptions and listeners.
- [Peer Dependencies](./peer-dependencies) - Used to declare indirect or optional dependencies between extensions, this can be used to enable some functionality or configuration only when some other extension is available. For example, an Emoji extension may declare a specific Markdown serialization for EmojiNode, but only if the Markdown extension is present.

---

### Packages Lexical Website Docs React Index (`packages/lexical-website/docs/react/index.md`)

---
sidebar_label: 'Introduction'
---

# Lexical + React

To make it easier for React users to implement rich-text editors, Lexical exposes a set of abstractions that aim to cover a wide range of use cases out-of-the-box.

<a
    class="whitespace-nowrap rounded-md bg-blue-500 px-6 py-2 text-sm font-bold text-white transition-opacity hover:text-white hover:no-underline hover:opacity-90"
    href="/docs/getting-started/react">
{`Getting Started Guide`}
</a>

## Supported Versions

React 18+ (tested with React 18 and React 19)

---

### Packages Lexical Website Docs Design (`packages/lexical-website/docs/design.md`)

# Lexical's Design

> Note: this document is still a work-in-progress and things are likely still in flux.

Lexical was built from the ground up to be a lean and extensible text-editing framework.

You can think of Lexical as more of a text editing engine, rather than a monolithic text
editor that provides everything out-of-the-box. It might help to think of Lexical as
a framework like React – where React provides some hooks and a reconciler, and then you
build the things you need on top, rather than have them included by default.

The main concept of Lexical is the editor state. Lexical uses a double-buffering technique
to ensure consistency and reliability. There are never more than two editor states in play.
The "current" editor state represents what you can visibly see on screen and the "pending"
editor state is what is currently being constructed to be shown in the future. Once the
"pending" editor state is ready, it swaps and becomes the new "current" editor state.

Lexical's core concerns itself with four main concerns:

- Updates: the act of making changes to editor state
- Node Transforms: the process of acting on ongoing updates
- Reconciliation: the process of patching the DOM with the latest editor state
- Listening/Commands: the process of reacting to changes that occur internally

Additionally, Lexical uses DOM mutation observers to ensure that any outside changes to
the editor DOM element are either reverted back to Lexical's current editor state, or are
communicated as intents that cause further updates to the editor state (text changes).

This separation was done to ensure developers have a way of applying custom logic with the
implementation and framework of their choice. This makes it possible to use Lexical with
React, or any other JavaScript web framework/library. This also makes it easier to tackle
problems around collaboration and undo/redo, where you might have an alternative model involved.
Instead of hoping for the text editor to support what you want, you can work with the editor
to make what you want.

This design can make getting started a bit more complex in certain cases, but as we've been
able to demonstrate with our `@lexical/react` plugins and hooks – you can get started, in the
form of an out-of-the-box experience.

---

### Packages Lexical Website Docs Error (`packages/lexical-website/docs/error.md`)

---
id: error
title: Error Code
---
import ErrorCodePage from '@site/src/components/ErrorCodePage';

# Error Code
<ErrorCodePage/>

---

### Packages Lexical Website Docs Faq (`packages/lexical-website/docs/faq.md`)

---
sidebar_position: 7
---

# FAQ

## Why does Lexical use the `$` prefix in the name of many of the functions?

Originally, Lexical didn't have `$` functions, instead these functions were provided to you through callback params:

```js
// Mid 2020 API
editor.update((viewState) => {
  const getRoot = viewState.getRoot();
  ..
});
editor.addTextTransform((viewState) => {
  const getRoot = viewState.getRoot();
  ..
});
```

Internally, this approach raised some negative feedback:

- The `viewState` terminology was confusing. It wasn't really a `viewState`, more like a toolkit to manipulate the `EditorState`
- For complex updates and transforms devs had to carry params around on many layers

This is when we decided to leverage the "lexical" scope instead to perform `EditorState` manipulation, and the `$` represents just that.

```js
editor.update(() => ...);
editor.registerNodeTransform(FooNode, () => ...);
editor.getEditorState().read(...);
```

If you've used React Hooks before, you can think of `$` functions as being something that follows a similar pattern. These are functions that show their intent as to where they can or cannot be used. This makes it possible for a developer to create their own functions that give the same signal, by simply prefixing the function with the dollar.

Internally, we've found this scales really well and developers get to grips with it in almost no time at all.

## When does reconciliation happen?

Reconciliation is scheduled with
[queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask),
which means that it will happen very soon, but asynchronously. This is similar
to something like `setTimeout(reconcile, 0)` with a bit more immediacy or
`Promise.resolve().then(reconcile)` with less overhead. This is done so
that all of the updates that occur as a result of a single logical event will
be batched into one reconciliation.

You can force a reconciliation to take place synchronously with the discrete
option to `editor.update` (demonstrated below).

## Why do tests use `await editor.update(…)`

You may notice that many tests look like this:

```js
await editor.update(updateA);
await editor.update(updateB);
```

An astute observer would notice that this seems very strange, since
`editor.update()` returns `void` and not `Promise<void>`. However,
it does happen to work as you would want it to because
the implementation of Promise uses the same microtask queue.

It's not recommended to rely on this in browser code as it could depend on
implementation details of the compilers, bundlers, and VM. It's best to stick
to using the `discrete` or the `onUpdate` callback options to be sure that
the reconciliation has taken place.

Ignoring any other microtasks that were scheduled elsewhere,
it is roughly equivalent to this synchronous code:

```js
editor.update(updateA, {discrete: true});
editor.update(updateB, {discrete: true});
```

At a high level, very roughly, the order of operations looks like this:

1. `editor.update()` is called
2. `updateA()` is called and updates the editor state
3. `editor.update()` schedules a reconciliation microtask and returns
4. `await` schedules a resume microtask and yields control to the task executor
5. the reconciliation microtask runs, reconciling the editor state with the DOM
6. the resume microtask runs

## How do I listen for user text insertions?

Listening to text insertion events is problematic with content editables in general. It's a common source of bugs due to how
different browsers and third-party extensions interact with the DOM. Whilst it's possible to use DOM events like `input` and
`beforeinput` to gauge some of the possible cases where a user has inserted text, these are hardly reliable and also don't
take into account edge-cases. Instead, Lexical prefers to consider any change as a possible user input, and as such doesn't
make a distinction between the cases. This is important for tools like spellcheck, browser extensions, IME, speech-to-text,
screen readers and other external tools that often don't reliably trigger a reliable event sequence (some don't even trigger
any events at all!).

For those wanting to react to a text change and possibly block/alter the intent, the recommended approach is to use a node
transform. This also plays nicely with other sub-systems at play that might also be looking to do the same thing as you.

For those who just want to know of the changes, this can be achieved using a text content listener or an editor update listener.

## How do I clear the contents of the editor?

You can go this by calling ```clear()``` on the RootNode in an update callback:

```js
editor.update(() => {
  $getRoot().clear();
})
```

## How do I listen to specific key down events?

You can leverage Lexical's command listening system. Lexical provides specific commands for many common keyboard operations, such as:

- `KEY_ARROW_LEFT_COMMAND`
- `KEY_ARROW_RIGHT_COMMAND`
- `KEY_ARROW_UP_COMMAND`
- `KEY_ARROW_DOWN_COMMAND`
- `KEY_SPACE_COMMAND`
- `KEY_ENTER_COMMAND`
- `KEY_BACKSPACE_COMMAND`
- `KEY_DELETE_COMMAND`
- `KEY_TAB_COMMAND`
- `KEY_ESCAPE_COMMAND`

```js
import {KEY_ENTER_COMMAND, COMMAND_PRIORITY_LOW} from 'lexical';

editor.registerCommand(KEY_ENTER_COMMAND, (event: KeyboardEvent) => {
  // Handle enter key presses here
  return false;
}, COMMAND_PRIORITY_LOW);
```

You can use the generic `KEY_DOWN_COMMAND` command to listen
to all keydown events. Do note, that returning `true` in your listener will prevent any
other key based commands from firing, so in most cases you'll want to return `false` from
the command listener.

```js
import {KEY_DOWN_COMMAND, COMMAND_PRIORITY_LOW} from 'lexical';

editor.registerCommand(KEY_DOWN_COMMAND, (event: KeyboardEvent) => {
  // Handle event here
  return false;
}, COMMAND_PRIORITY_LOW)
```

:::tip

If you do return `true` from any listener with an event payload, you likely
also should call `event.preventDefault()` unless your command relies on the
browser's native processing of that event.

:::

## Why do I get "does not implement .getType()" or "does not match registered node"?

```
LexicalNode: Node FooNode does not implement .getType().
Create node: Type heading in node HeadingNode does not match registered node HeadingNode with the same type
```

These almost always mean your app resolved **two copies of `lexical`**, not that
the node is misdeclared — the `instanceof` and registration checks compare class
references, and the two copies have different ones. Run `npm ls lexical` (or
`pnpm why lexical`) from the app root: it must report exactly one version.
See [One Lexical per app](concepts/one-lexical-per-app.md) for the causes and
the fix, including why a library must declare `lexical` in `peerDependencies`.

## Which module formats are published? (ESM, CommonJS, Node.js, React Native)

Every Lexical package is published as ES modules only. The packages declare
`"type": "module"`, and their `exports` maps offer a `development` and a
`production` build plus a `default` entry that picks one of the two at runtime
from `process.env.NODE_ENV`.

- **Bundlers** (webpack, Vite, Rollup, esbuild with the conditions set) resolve
  the `development` or `production` condition and get exactly one build.
- **Node.js** resolves `default`, which loads both builds and exports one. To
  load a single build, pass the condition on the command line:
  `node --conditions=production app.js` (or `--conditions=development`).
- **CommonJS** code can `require()` the packages on Node.js 20.19 or later,
  which loads ES modules from `require()` as long as nothing in the module
  graph uses top-level `await` (Lexical's builds do not). Older Node.js
  versions have to use `await import('lexical')` instead. Loading is all
  that `require()` guarantees: Lexical's builds import their own
  dependencies as ES modules, so a dependency that a CommonJS application
  also loads through `require()` can end up in the application twice, once
  per module system, with separate classes and module state. That breaks
  any integration that hands such a dependency's objects to Lexical, for
  example `yjs` documents given to `@lexical/yjs`, or `@preact/signals-core`
  effects observing signals from `@lexical/extension`. Load those
  dependencies as ES modules too (`await import('yjs')`), or move the
  application to ES modules.
- **React Native (Metro)** bundles the packages without extra configuration,
  but Metro does not set the `development`/`production` conditions and does
  not drop the build the `default` entry leaves unused, so both end up in the
  bundle. To bundle only the one that matches the build mode, add the
  condition for Lexical's packages in `metro.config.js`:

  ```js
  const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');

  module.exports = mergeConfig(getDefaultConfig(__dirname), {
    resolver: {
      resolveRequest: (context, moduleName, platform) =>
        context.resolveRequest(
          /^(lexical|@lexical\/)/.test(moduleName)
            ? {
                ...context,
                unstable_conditionNames: [
                  ...context.unstable_conditionNames,
                  context.dev ? 'development' : 'production',
                ],
              }
            : context,
          moduleName,
          platform,
        ),
    },
  });
  ```

---

### Packages Lexical Website Docs Maintainers Guide Link (`packages/lexical-website/docs/maintainers-guide-link.md`)

---
sidebar_position: 99
---

# Developing against a local Lexical checkout

If you're iterating on Lexical itself and want a downstream app to pick
up your changes without publishing, the Lexical packages support two
linking workflows.

Both rely on the fact that each `packages/<name>/` directory is its own
publishable npm package: after a build, `packages/lexical/package.json`
plus the `dist/` it sits next to is a complete tarball. There is no
intermediate `npm/` directory and no copy step — `pnpm pack` /
`pnpm publish` run straight from the package root.

## Option A: built artifacts (works for any consumer)

Build the artifacts, then point your downstream project at the package
directory:

```bash
# in the Lexical checkout
pnpm install
pnpm run build-release   # produces dist/<Name>.{dev,prod}.js + .d.ts + .js.flow

# in your downstream app
pnpm add link:/path/to/lexical/packages/lexical
pnpm add link:/path/to/lexical/packages/lexical-react   # etc.
```

`pnpm` follows the `link:` protocol by creating a real symlink into your
`node_modules/`, so subsequent rebuilds of Lexical (e.g.
`pnpm run build` in the Lexical checkout) become visible immediately.
Standard `exports` resolution picks up the `development` /
`production` conditions, so dev builds load the unminified variants and
prod builds load the minified ones automatically. Only ESM is published;
a CommonJS consumer loads the same files through `require(esm)`
(Node.js 20.19+).

If you prefer the dev-only loop (faster builds, no minification), use
`pnpm run build` instead of `build-release`. The fork modules emitted
by a dev-only build re-export the `.dev` variant unconditionally; you
won't be able to `pnpm publish` from such a checkout because the
publish guard will report missing `.prod` files.

`file:/path/to/...` works too if your package manager treats it as a
symlink (modern pnpm does); npm copies the directory into
`node_modules` instead, which loses any live-reload benefit but is
otherwise equivalent for one-shot installs.

## Option B: TypeScript source (no Lexical build needed)

For the tightest dev loop — edit a Lexical source file and have your
downstream bundler pick it up on the next request — opt into the
`source` export condition. Each public package's `exports` map exposes
its `./src/<entry>.tsx?` next to the compiled `development`/`production`/
`default` conditions, so a bundler configured with
`resolve.conditions: ['source', …]` will load TypeScript directly out
of the linked package.

The only requirement is **bundler support**: your consumer's bundler has to
be configured to pick up the `source` condition AND to transform
`.ts`/`.tsx` from inside `node_modules`. Vite, Rspack, and Parcel can all be
told to do this; webpack needs a loader override for the linked package.

There is **no aliasing and no extra `define` to set up**. Internal utilities
the source depends on are a real (internal) dependency, `@lexical/internal`,
which resolves through normal package resolution; the React/test helpers are
package-internal; and the dev/prod branch is `process.env.NODE_ENV !==
'production'`, which every mainstream bundler substitutes by default. So
`source` mode works without any `resolve.alias` entries or build-time globals
— including for the transitive `@lexical/*` packages.

One thing the compiled bundles have that the sources do not is the
`/* @__PURE__ */` annotations on module-scope calls to `defineExtension`,
`createCommand`, `createState`, and friends. Lexical's own build injects
them, and without them a bundler has to assume those calls have side effects
and will keep every extension, command, and rule definition your app never
uses. Add [`@lexical/compiler`](https://www.npmjs.com/package/@lexical/compiler)
to your build to get the same annotations out of `source` mode — it annotates
the factory calls in your own code too.

### Minimal Vite setup

```ts
// vite.config.ts
import {pureAnnotations} from '@lexical/compiler/PureAnnotations';
import {defineConfig} from 'vite';

export default defineConfig({
  plugins: [pureAnnotations()],
  resolve: {
    conditions: ['source', 'development', 'module', 'browser', 'default'],
  },
});
```

Because a `link:`ed checkout is the same version as the plugin, you can also
pass `pureAnnotations({inline: true})`, which replaces calls to the trivial
factories (`safeCast`, `defineExtension`, `configExtension`, ...) with the
literal they would have returned rather than annotating them.

`@lexical/compiler/PureAnnotations` also exports the transform itself
(`transformPureAnnotations`) for bundlers without a Rollup-style plugin API;
see its README for the options.

The integration fixture at
`scripts/__tests__/integration/fixtures/lexical-link-source-mode/`
exercises exactly this setup and is the canonical reference if you need
a fuller example.

> [!NOTE]
> `@lexical/internal` is published only so this resolution works (and for
> direct internal consumers); the compiled `dist/` artifacts inline it, so
> a normal `npm install` of `lexical` never executes a separate copy.

---

### Packages Lexical Website Docs Maintainers Guide (`packages/lexical-website/docs/maintainers-guide.md`)

# Maintainers' Guide

This is the grimoire of arcane knowledge covering the overall organization
of the Lexical monorepo, including its conventions, quirks, and
configurations.

## Monorepo Organization

### Workspaces

The monorepo is configured with
[pnpm workspaces](https://pnpm.io/workspaces); the set of workspace
packages is declared in the top-level `pnpm-workspace.yaml`. This mostly means that all packages share a
top-level `pnpm-lock.yaml` and `pnpm -C {package} run {command}` is often
used to run a command from a nested package's package.json.

### Private

Some packages in the monorepo do not get published to npm, for example:

* `packages/lexical-devtools` - browser extension for working with Lexical
  sites
* `packages/lexical-playground` - the
  [playground.lexical.dev](https://playground.lexical.dev/) demo site
* `packages/lexical-website` - the [lexical.dev](https://lexical.dev/)
  docusaurus website that you may even be reading right now
Internal runtime code shared by more than one package lives in
`packages/lexical-internal` (`@lexical/internal`). Unlike the others above
it **is** published, but only so its source resolves through normal package
resolution (the `source` export condition used by linked-checkout
consumers); the compiled packages inline it, so it is never executed as a
separate runtime dependency. It is not a public API and has no semver
guarantees — see [Developing against a local Lexical
checkout](./maintainers-guide-link.md).

It is required that private packages, and any other package that should not
be published to npm, have a `"private": true` property in their `package.json`.
If you have an in-progress package that will eventually be public, but is
not ready for consumption, it should probably still be set to
`"private": true` otherwise the tooling will find it and publish it.

## Package naming conventions

### Overall

| Usage | Convention |
| -- | -- |
| Directory name | `packages/lexical-package-name` |
| Entrypoint | `packages/lexical-package-name/src/index.{ts,tsx}` |
| Flow types | `packages/lexical-package/flow/LexicalPackageName.js.flow` |
| package.json name | `@lexical/package-name` |
| Documentation | `packages/lexical-package-name/README.md` |
| Unit Tests | `packages/lexical-package-name/src/__tests__/unit/LexicalPackageName.test.{ts,tsx}` |
| dist (gitignore'd build product) | `packages/lexical-package-name/dist` |
| npm (gitignore'd prerelease product) | `packages/lexical-package-name/npm` |
| www entrypoint | `packages/lexical-package-name/LexicalPackageName.js` |

### Multiple module export (@lexical/react)

Instead of having a single module, some packages may have many modules
(currently only `@lexical/react`) that are each exported separately.
In that scenario, there should be no `index.ts` entrypoint file and every module
at the top-level should be an entrypoint. All entrypoints should be a
TypeScript file, not a subdirectory containing an index.ts file.

The [update-packages](#pnpm-run-update-packages) script will ensure that the
exports match the files on disk.

## Creating a new package

The first step in creating a new package is to create the workspace directory
and package.json file. The example we will use is the steps that were used
to create the `lexical-eslint-plugin`, which will be published to npm as
`@lexical/eslint-plugin`.

### Create the workspace

```bash
mkdir -p packages/lexical-eslint-plugin
```

Create the initial `package.json` file (you can base it on an existing package
or use the template below):

<details><summary>

`packages/lexical-eslint-plugin/package.json`
</summary>

```json
{
  "name": "@lexical/eslint-plugin",
  "description": "",
  "keywords": [
    "lexical",
    "editor"
  ],
  "version": "0.14.3",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/facebook/lexical.git",
    "directory": "packages/lexical-eslint-plugin"
  },
  "main": "LexicalEslintPlugin.js",
  "types": "index.d.ts",
  "bugs": {
    "url": "https://github.com/facebook/lexical/issues"
  },
  "homepage": "https://github.com/facebook/lexical#readme"
}
```
</details>

Some next steps for this package.json before moving on:

- Update the description
- Add appropriate keywords

### Create the initial source file

```
mkdir -p packages/lexical-eslint-plugin/src
code packages/lexical-eslint-plugin/src/index.ts
```

Here are some minimal examples of those files that you might start out with.
I've elided the license header, the eslint header/header fixer will help you
with that!

<details><summary>

`packages/lexical-eslint-plugin/src/index.ts`
</summary>

```typescript
import {name, version} from '../package.json';

const plugin = {
  meta: {name, version},
  rules: {},
};

export default plugin;
```
</details>

### Run update-packages to generate boilerplate docs & config

```
pnpm run update-packages
```

This will set up the tsconfig, flow, etc. configuration to recognize your
new module. It will also create an initial README.md using only the
description from the package.json.


### Create an initial unit test

```
mkdir -p packages/lexical-eslint-plugin/src/__tests__/unit
code packages/lexical-eslint-plugin/src/__tests__/unit/LexicalEslintPlugin.test.ts
```


<details><summary>

`packages/lexical-eslint-plugin/src/__tests__/unit/LexicalEslintPlugin.test.ts`
</summary>

```typescript
import plugin from '@lexical/eslint-plugin';

describe('LexicalEslintPlugin', () => {
  it('exports a plugin with meta and rules', () => {
    expect(Object.keys(plugin).sort()).toMatchObject(['meta', 'rules']);
  });
});
```
</details>


## Scripts for development

### pnpm run update-packages

This script runs: update-version, update-tsconfig, update-flowconfig,
create-docs, and create-www-stubs. This is safe to do at any time and will
ensure that package.json files are all at the correct versions, paths are
set up correctly for module resolution of all public exports, and that
various defaults are filled in.

These scripts can be run individually, but unless you're working on one
of these scripts you might as well run them all.

### pnpm run prepare-release

This runs `build-release` to produce all of the artifacts each public
package needs (the `dev`/`prod` ESM variants plus their fork modules,
`.d.ts` declarations, and `.js.flow` stubs under
`packages/<name>/dist/`), then runs the publish-time guard in
`scripts/npm/prepare-release.mjs` to confirm every path the package's
`exports`/`main`/`module`/`types` fields reference actually exists on
disk. The guard fails the build if e.g. you ran `pnpm run build` (dev
only) and then tried to publish — the `.prod.js` files would be
missing.

Only ESM is published to npm. Every public package is a
`"type": "module"` package (`update-version` adds the field), so its
build is plain `.js`. A CommonJS consumer gets the same files through
Node's `require(esm)` (Node.js 20.19+), which is why the fork module that
the exports map's `default` condition resolves to has no top-level await.
The CommonJS variants are only built for www (`pnpm run build-www`).

Because a `.d.ts` in a `"type": "module"` package is an ES module
declaration, the build gives the relative imports tsc emits an explicit
`.js` extension (`from './LexicalEditor.js'`): TypeScript's `nodenext`
resolution does not resolve extensionless relative imports in ESM, and a
consumer with `skipLibCheck` would silently get `any` for everything.
`scripts/__tests__/integration/declaration-resolution.test.mjs` checks
that a `nodenext` consumer of every entry point still sees the types.

Each package is its own publish root: `packages/<name>/` IS the
publishable npm package after `build-release`. `pnpm publish` is run
directly from that directory by `scripts/npm/release.mjs` so pnpm's
automatic `workspace:*` rewriting and the `files` whitelist do the
right thing without an intermediate `npm/` copy step.

This will also update `scripts/error-codes/codes.json`, the mapping of
production error codes to error messages. It's imperative to commit the
result of this before tagging a release.

### pnpm run ci-check

Check flow, TypeScript, prettier and eslint for issues. A good command to run
after committing (which will auto-fix most prettier issues) and before pushing
a PR.

### pnpm run flow

Check the Flow types

### pnpm run tsc

Check the TypeScript types

### pnpm run tsc-extension

Check the TypeScript types of the lexical-devtools extension

### pnpm run test-unit

Run the unit tests

### pnpm run lint

Run eslint

### pnpm run generate-node-json

Regenerate the specialized JSON serialization code for the built-in node
classes. A node's [serialization schema](/docs/serialization/#declarative-serialization-schemas-with-config)
states everything about a serialized property ahead of time — which accessor
or field it uses, what its default is, what its domain admits — so the
generic walk over that schema can be compiled into straight-line code. This
script does that compiling; the output is checked in.

It writes one module per package, beside the nodes it serializes:

| Module | Classes |
| --- | --- |
| `packages/lexical/src/LexicalGeneratedJSON.ts` | ElementNode, TextNode, ParagraphNode, LineBreakNode, TabNode |
| `packages/lexical-rich-text/src/LexicalRichTextGeneratedJSON.ts` | HeadingNode, QuoteNode |
| `packages/lexical-link/src/LexicalLinkGeneratedJSON.ts` | LinkNode, AutoLinkNode |
| `packages/lexical-mark/src/LexicalMarkGeneratedJSON.ts` | MarkNode |
| `packages/lexical-list/src/LexicalListGeneratedJSON.ts` | ListNode, ListItemNode |
| `packages/lexical-table/src/LexicalTableGeneratedJSON.ts` | TableNode, TableRowNode, TableCellNode |
| `packages/lexical-code-core/src/LexicalCodeCoreGeneratedJSON.ts` | CodeNode, CodeHighlightNode |
| `packages/lexical-react/src/shared/LexicalReactGeneratedJSON.ts` | DecoratorBlockNode |

Each class receives its own generated code through its `$config`'s
`generated` property, so nothing has to match code to class by type string at
runtime. What the property holds is a factory: registration calls it with the
class's composed schema, and the generated code reads the lookup tables it
needs (`getterTable`, `setterTable`, `aliasedValue`'s) off that schema through
`getterTableOf`, `setterTableOf` and `aliasTableOf`, so a generated module
carries no copy of a table and nothing about a table's contents is written at
build time — only its type, so the field a value is assigned to is still
checked. A subclass inherits the code along with the schema when its compiled
accessor tables are the ones the code was generated from — checked entry for
entry at registration — and runs it over its own schema, while one that
overrides an accessor a field stands in for, or declares a serialized property
of its own, resolves differently and takes the schema-driven walk instead.
Generated exporters read `type` off the node for the same reason.

Three things are worth knowing before touching it:

- **The output is verified, not trusted.** The import direction is the
  untrusted-JSON boundary, so every generated parser is run against the schema
  it was compiled from over a corpus drawn from that schema plus a fixed set of
  hostile values (`'__proto__'`, `'toString'`, `'1e999'`, …). A property whose
  schema cannot be compiled faithfully takes its class out of the import half
  rather than shipping a parser that disagrees with the walk — the script says
  so on stdout when it happens.
- **A stale checkout fails the tests.** `LexicalGeneratedJSON.test.ts`
  regenerates into a temporary directory and compares every file byte for
  byte, and separately asserts that each generated exporter agrees with the
  schema-driven walk for both the legacy and compact forms. Change a schema
  without rerunning this script and that test fails.
- **Two lists, deliberately.** The script runs in two phases, because reading
  the schemas means importing the packages and each package imports the file
  the script writes for it. Phase one replaces every output with a valid
  do-nothing stub from the static `MANIFEST` in
  `scripts/shared/generateNodeJSONManifest.mjs` so the imports always succeed;
  phase two re-enters under `tsx` and writes the real thing from `PACKAGES` in
  `scripts/shared/generateNodeJSON.mjs`, which is also what the drift test
  runs in-process. Adding a class means editing both lists, and the generation
  fails loudly if they disagree.

The compact form compares each property against its default. A primitive
default is a literal; a reference-typed default has no literal a value could
be `===`, so it gets the structural test the schema's own equality reduces to
where that can be stated — MarkNode's `ids`, whose default is an empty array,
becomes a length test — and every emitted comparison is verified against that
equality over a corpus, the way a parse is. A default the generator cannot
state that way (an object, a non-empty array, a non-finite number) takes the
class out of the compact half only: the script says so on stdout and the other
forms are generated as usual. A class that carries flat NodeState is generated
like any other; the walk applies the state before handing the node to the
generated parser, the mirror of how export appends it after the generated
literal.

`afterCloneFrom` is generated too, and is the one direction whose fallback is
not the walk. A schema field is where a property is *stored*, so every class
that declares one gets an `afterCloneFrom` synthesized at registration —
generated straight-line code when the class has some, and otherwise a loop over
the field names — copying the fields that class's own `$config` declared and
delegating the rest through `super`, which is why the emitted function covers
one class's own fields and nothing above it. A field an ancestor declares too
is left to the ancestor, whose method has already assigned it: re-declaring an
inherited property changes how it is serialized, not where it is stored, so a
class that only re-declares gets no method at all — `TabNode`, which restates
`TextNode`'s `text`, `detail` and `mode`, is the in-tree case and simply
inherits `TextNode`'s. Both accessor directions are read
for a field name, and the declared field is used rather than the one
`resolveGetterAccessor` resolves to: an override changes how a property is
serialized, not where it lives. A class that writes its own `afterCloneFrom` is
left alone and owns all of its properties, which is how `ElementNode` keeps
carrying `__first`/`__last`/`__size` and its slot bookkeeping; so is a property
declared through accessor methods on both sides, which names no field for
anything to copy. No in-tree node is in that second position — a property held
in a field says so with `setter: {field, method}` and stays derived, as
`MarkNode`'s `ids` does — so the boilerplate that remains is `ElementNode`'s
and `CodeNode`'s, and each of those calls the generated `afterClone<Class>`
for its schema half and writes only the fields no schema describes.
`ownSchemaFields` in `LexicalUtils.ts` is the single definition of that field
list, called by both the generator and the synthesized fallback.

The schema-to-JavaScript compiler itself lives in `@lexical/compiler`'s
`SchemaJsonCodegen` entry point, so it is testable independently of the
generator that drives it.

## Scripts for release managers

### pnpm run extract-codes

This will run a build that also extracts the generated error codes.json file.

This should be done, at minimum, before each release, but not in any PR as
it would cause conflicts between serial numbers.

It's safe and probably advisable to do this more often, possibly any time a
branch is merged to main.

The codes.json file is also updated any time a release build is generated
as a failsafe to ensure that these codes are up to date in a release.
This command runs a development build to extract the codes which is much
faster as it is not doing any optimization/minification steps.

### pnpm run increment-version

Increment the monorepo version. The `-i` argument must be one of
`minor` | `patch` | `prerelease`.

The postversion script will:
- Create a local `${CHANNEL}__release` branch (the version-named
  `${npm_package_version}__release` branch is only a remote push target,
  and only when the channel is not `nightly`)
- `pnpm run update-version` to update example and sub-package monorepo dependencies
- `pnpm install` to update the pnpm-lock.yaml
- `pnpm run update-packages` to update other generated config
- `pnpm run extract-codes` to extract the error codes and
  `pnpm run update-changelog` to update the changelog (both only when
  `CHANNEL` is `latest`)
- Create a version commit and tag from the branch

This is typically executed through the `version.yml` GitHub Workflow which
will also push the tag and branch.

### pnpm run changelog

Update the changelog from git history.

### pnpm run release

*Prerequisites:* all of the previous release manager scripts,
plus creating a tag in git, and likely other steps.

Runs prepare-release to do a full build and then uploads to npm.

### pnpm run setup-trusted-publishing

One-time (idempotent) helper to register every public package with
[npm trusted publishing](https://docs.npmjs.com/trusted-publishers).
Re-run it whenever a new public package is added.

#### Prerequisites

- Node.js — whatever the repo's root `package.json#engines.node` says (currently `>=20.19.0`). Running with Node 24+ is recommended because that's what CI uses for publishes.
- pnpm — pinned by `package.json#packageManager` (currently `pnpm@11.24.0`). Activate with [corepack](https://nodejs.org/api/corepack.html) or install directly.
- npm CLI — **`npm ≥ 11.10`** (`npm i npm@latest`). The `npm trust` subcommand was added in npm 11; older versions will fail the preflight check.
- An authenticated npm session (`npm login --registry https://registry.npmjs.org`) on a publisher account that has **account-level 2FA enabled** and write access to every `@lexical/*` package.

#### Usage

Run in check-only mode first:

```bash
pnpm run setup-trusted-publishing
```

For each public package in the monorepo, it queries
`https://registry.npmjs.org` and reports whether the name is already
claimed. Packages that *don't* exist on the registry are listed; you
can re-run with `--bootstrap` to publish a deprecated
`0.0.0-bootstrap.0` placeholder under the `bootstrap` dist-tag so the
name can be claimed:

```bash
npm login --registry https://registry.npmjs.org
pnpm run setup-trusted-publishing --bootstrap
```

Once a package exists on the registry, you can configure trusted
publishing for it programmatically by adding `--setup-trust`. This
runs `npm trust github` under the hood (requires `npm` ≥ 11.10 and an
authenticated session with account-level 2FA on the publishing
account), and is idempotent — the script reads the existing trust
configuration for each package via a read-only registry call (no OTP)
and skips packages whose config already matches:

```bash
npm login --registry https://registry.npmjs.org
pnpm run setup-trusted-publishing --setup-trust
```

`npm trust github` is a write operation, so each package that *does*
need configuring will trigger a one-time-password / web-auth prompt.
On the first prompt npm prints a URL; open it in a browser, sign in,
and tick **"Skip two-factor authentication for the next 5 minutes"**.
Subsequent packages in the same run will then go through without
re-prompting. The script also inserts a small (~2 s) pause between
calls to stay under the registry's `E429` rate limit.

For full first-time setup of a brand-new monorepo, combine both flags:

```bash
pnpm run setup-trusted-publishing --bootstrap --setup-trust
```

When adding a **single** new package to an existing monorepo — the common
case going forward — pass its name so the run only touches that package
instead of re-checking all 30+ already-configured ones (which just prints a
wall of `CONFLICT` lines). The name can be the full npm name or the unscoped
short name, and `--package` / positional args are interchangeable and
repeatable:

```bash
pnpm run setup-trusted-publishing --bootstrap --setup-trust @lexical/a11y
# equivalently: --package a11y
```

Useful flags:

- `--package <name>` (or a positional `<name>`, repeatable) — restrict the run to the given package(s), matched by full npm name (`@lexical/a11y`) or unscoped short name (`a11y`). Omit to process every public package.
- `--dry-run` — print what would happen without touching the registry (works with both `--bootstrap` and `--setup-trust`)
- `--workflow <filename>` — override the workflow filename (default `pre-release.yml`)
- `--repo <owner/name>` — override the GitHub repo (default `facebook/lexical`)
- `--stub-version <semver>` — override the placeholder version (default `0.0.0-bootstrap.0`)
- `--registry <url>` — override the npm registry

In the default (check-only) mode the script also prints the npmjs.com
`/access` URL for each existing package and the exact values to enter
manually, as a fallback for when `npm trust github` isn't an option.

### Testing trusted publishing from a PR branch

The "Publish to NPM" workflow (`pre-release.yml`) exposes `ref`,
`channel`, and `increment-version` inputs so it doubles as a test
harness. Picking a branch in the "Run workflow" dropdown selects
which version of the workflow files run, and the inputs determine
what actually gets published. The workflow has no NPM_TOKEN secret to
fall back on — publishes always go through OIDC trusted publishing —
so a misconfigured trust setup fails loudly rather than silently
falling through to token auth.

A safe end-to-end test looks like:

| Input | Value |
| -- | -- |
| Branch (dropdown) | your PR branch |
| `ref` | your PR branch (same value) |
| `channel` | `dev` |
| `increment-version` | checked |
| `ignore-previously-published` | unchecked |

With `increment-version` on, the run bumps `package.json` to a fresh
prerelease (e.g. `0.46.0-dev.0`), commits + tags it on a `dev__release`
branch on origin, and publishes the monorepo under the `dev` dist-tag
via OIDC. The `latest` tag is untouched, so default `npm install`
users are unaffected. After it succeeds:

```bash
npm view lexical@dev version     # → the just-published prerelease
npm view lexical@latest version  # → unchanged
```

Cleanup (the prerelease itself can't be reused, but the git refs
should go):

```bash
git push --delete origin v0.46.0-dev.0 dev__release 0.46.0-dev.0__release
```

The `increment-version=true + channel=latest` combination is refused
by the workflow's guard job — real `latest` releases must go through
`version.yml` first.

## Continuous releases (pkg.pr.new)

Every commit to a pull request and every commit to `main` publishes all public
packages to [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) through
the `pkg-pr-new.yml` workflow. Nothing goes to npm: the nightly, next and latest
channels above are unchanged and still publish through `call-release.yml`.

```sh
npm i https://pkg.pr.new/lexical@9372      # latest commit of PR #9372
npm i https://pkg.pr.new/lexical@4282a68   # a specific commit
npm i https://pkg.pr.new/lexical@main      # tip of main
```

The bot comments install commands on each pull request. Previews are built
with `pnpm run prepare-release`, the same build as the npm channels, after
`scripts/npm/preview-version.mjs` stamps a version in the nightly line:
`0.52.1-pr.9372.g4282a68` for a PR and `0.52.1-main.g4282a68` for `main`, next
to `0.52.1-nightly.20261019.0` on npm. That version is also what
`LexicalEditor.version` reports. Dependencies between Lexical packages are
rewritten to the same commit's pkg.pr.new URLs, so `@lexical/react` from a
preview depends on `lexical` from that preview, not on an npm release.

The bot's comment lists npm commands, and npm dedupes those dependencies by
name and version, so `@9372` or `@main` URLs are fine. pnpm and yarn key URL
dependencies by URL instead: install with the long commit URLs that the
packages use for each other, or you get more than one copy of `lexical`. pnpm
11+ also refuses URL dependencies of URL dependencies unless
`blockExoticSubdeps` is off. Each run's job summary lists these commands:

```sh
pnpm add --config.block-exotic-subdeps=false \
  lexical@https://pkg.pr.new/facebook/lexical/lexical@4282a68 \
  @lexical/react@https://pkg.pr.new/facebook/lexical/@lexical/react@4282a68
yarn add \
  lexical@https://pkg.pr.new/facebook/lexical/lexical@4282a68 \
  @lexical/react@https://pkg.pr.new/facebook/lexical/@lexical/react@4282a68
```

On a cold cache, yarn 1 can fail with "the file appears to be corrupt" while
it downloads the same URL for several packages at once. Run it again.

The workflow requires the [pkg.pr.new GitHub App](https://github.com/apps/pkg-pr-new)
to be installed on the repository.

## Release Procedure

This is the current release procedure for public releases, at least as of
May 2024 (~0.15.0).

The main branch should be "frozen" during this procedure (no other PRs should
be merged during this time). This avoids a mismatch between the contents of
the GitHub release (created from main in step 1) and the NPM release (created
from main in step 4).

1. Create a new version with the Github Actions "Create New Release Branch" workflow (`version.yml`)
2. Raise a PR against version branch created by that action
3. After PR is approved with passing tests, merge PR
4. After PR is merged to main, publish to NPM with the Github Actions "Publish to NPM" workflow (`pre-release.yml`)
5. Create a GitHub release from the tag created in step 1, manually editing the release notes
6. Announce the release in #announcements on Discord

## Release Protocol

1. All PRs with breaking changes must have `[Breaking Change]` in the PR's title with documentation of what followup actions consumers of the lexical library need to be aware of.
2. Monthly releases happen on the last week of the month, with a minor increment (eg. v0.20+1.0).
3. Anything in between will be a patch increment (eg. 0.20.0+1), unless there is a breaking change. 

## Website Team Page

The [team page](https://lexical.dev/community) displays core team
members, emeriti, and distinguished contributors. The `team.json` data is
generated from GitHub contributor information and some predetermined decisions in
the script to acknowledge emeriti and historically important distinguished contributors.

To update the team page data:

```bash
pnpm run update-team-data
```

This fetches the latest contributor data from GitHub and categorizes team members
based on recent activity (last 12 months). See `packages/lexical-website/src/data/README.md`
for more details on configuration and team categorization logic.

---

### Packages Lexical Website Docs Testing (`packages/lexical-website/docs/testing.md`)

---
sidebar_position: 6
---

# Testing

Lexical has three tiers of tests: unit tests in jsdom, browser tests in a real browser, and end-to-end tests against the playground. Because `contentEditable` doesn't work in jsdom, anything involving real user input needs a browser.

## Unit tests (jsdom)

Unit tests run in [vitest](https://vitest.dev/) with a jsdom environment. They are fast (the full suite finishes in ~20 seconds) and cover the majority of Lexical's API surface.

```bash
pnpm test-unit
```

Test files live under `packages/**/src/__tests__/unit/` and `packages/**/__tests__/unit/`.

### Why user input doesn't work in jsdom

Calls like `userEvent.type()` or `fireEvent.input()` from React Testing Library will not produce any effect:

:::info

jsdom does not implement `contentEditable` editing. In a real browser, a keystroke inside a `contentEditable` element goes through the browser's native editing engine, which modifies the DOM and fires `beforeinput`/`input` events that Lexical observes. jsdom has no such engine, so dispatching synthetic `InputEvent`s has no effect.

The same applies to ProseMirror, Slate, Tiptap, and any other `contentEditable`-based editor. The approach is the same everywhere: use the editor's API to manipulate state in unit tests, and use a real browser when you need real input.

:::

### What you can test in jsdom

Anything that goes through Lexical's API rather than the browser's input pipeline:

```ts
import {buildEditorFromExtensions} from '@lexical/extension';
import {RichTextExtension} from '@lexical/rich-text';
import {
  $createParagraphNode,
  $createTextNode,
  $getRoot,
  $getSelection,
  $isRangeSelection,
  CONTROLLED_TEXT_INSERTION_COMMAND,
} from 'lexical';

const editor = buildEditorFromExtensions({
  $initialEditorState: () => {
    $getRoot().append(
      $createParagraphNode().append($createTextNode('hello')),
    );
  },
  dependencies: [RichTextExtension],
  name: 'test',
});

const root = document.createElement('div');
root.contentEditable = 'true';
document.body.appendChild(root);
editor.setRootElement(root);
```

**Insert text via the selection API:**

```ts
editor.update(() => {
  $getRoot().selectEnd();
}, {discrete: true});

editor.update(() => {
  const selection = $getSelection();
  if ($isRangeSelection(selection)) {
    selection.insertText(' world');
  }
}, {discrete: true});

editor.read(() => {
  expect($getRoot().getTextContent()).toBe('hello world');
});
```

`{discrete: true}` makes the update commit synchronously instead of being batched, so each step's effects are visible to the next one.

**Insert text via a command.** This also requires a selection to be set first:

```ts
editor.dispatchCommand(CONTROLLED_TEXT_INSERTION_COMMAND, ' world');
```

Both approaches update the editor state and the DOM. Use whichever is closer to the code path you're testing.

### What you cannot test in jsdom

- Typing by dispatching `KeyboardEvent` or `InputEvent` on the DOM to insert text
- IME / composition sequences
- Selection changes driven by mouse clicks or arrow keys
- Clipboard paste with real browser `DataTransfer`

`KeyboardEvent` dispatch *does* work for non-text-input events. `KEY_ENTER_COMMAND`, `KEY_TAB_COMMAND`, `KEY_ARROW_*` commands, and shortcuts can all be tested in jsdom, either by dispatching the event on the DOM or via `editor.dispatchCommand`.

If your test needs real text input or composition, use browser tests.

### Editor cleanup with `using`

`buildEditorFromExtensions` returns an editor with a `dispose()` method and `Symbol.dispose` support. Declaring it with `using` automatically cleans up registrations at the end of the block:

```ts
it('inserts text', () => {
  using editor = buildEditorFromExtensions({
    dependencies: [RichTextExtension],
    name: 'test',
  });
  // editor.dispose() is called automatically at the end of the block
});
```

This only works in unit tests (jsdom). The `using` syntax is not supported in all browser engines, so browser tests should call `editor.dispose()` manually or use vitest's `onTestFinished` callback instead.

### Property-based tests with `@lexical/fast-check`

A node that declares a [serialization schema](./serialization/serialization.md#declarative-serialization-schemas-with-config) has already stated what each of its properties accepts, so tests do not have to invent examples. [`@lexical/fast-check`](./packages/lexical-fast-check.md) turns that declaration into a [fast-check](https://fast-check.dev) arbitrary:

```bash
npm install --save-dev @lexical/fast-check fast-check
```

```ts
import {nodeArbitrary} from '@lexical/fast-check';
import * as fc from 'fast-check';

fc.assert(
  fc.property(nodeArbitrary(MyNode), props => {
    // props is a serialized MyNode, drawn from its own schema's domain
  }),
);
```

This is worth reaching for whenever a hand-written fixture would tend to use *default* values, because a bug that drops a property compares equal to the default — the test looks like it passed exactly when the bug is invisible. The clearest case is `afterCloneFrom`: declaring a new property and forgetting to copy it there is silent, since the field still exists on the clone and simply holds the constructor's default. Generated values catch it, and fast-check shrinks the failure to the property at fault:

```
Counterexample: [{"style":" "}]

-   "style": " ",
+   "style": "",
```

The package's [README](./packages/lexical-fast-check.md) has the full example, and `packages/lexical-fast-check/src/__tests__/unit/` runs it against `TextNode`.

## Browser tests (vitest browser mode)

Browser tests run in a real browser via vitest's [browser mode](https://vitest.dev/guide/browser/), backed by Playwright. The browser has a real `contentEditable` engine, so input events, composition, and selection work as expected.

```bash
pnpm test-browser                                 # default: chromium
VITEST_BROWSER=firefox pnpm test-browser           # firefox
VITEST_BROWSER=chromium,firefox pnpm test-browser  # multiple browsers
```

Test files live under `packages/**/src/__tests__/browser/`. They look like normal vitest tests. Here's an example that uses the `compose()` and `korean()` helpers to simulate an IME sequence:

```ts
import {buildEditorFromExtensions} from '@lexical/extension';
import {RichTextExtension} from '@lexical/rich-text';
import {$getRoot, $createParagraphNode} from 'lexical';
import {expect, test, onTestFinished} from 'vitest';
import {compose, korean} from './utils/compose';

test('IME composition inserts Korean text', async () => {
  const editor = buildEditorFromExtensions({
    $initialEditorState: () => {
      $getRoot().append($createParagraphNode());
    },
    dependencies: [RichTextExtension],
    name: 'test',
  });
  const rootElement = document.createElement('div');
  rootElement.contentEditable = 'true';
  document.body.appendChild(rootElement);
  editor.setRootElement(rootElement);
  onTestFinished(() => {
    editor.setRootElement(null);
    rootElement.remove();
    editor.dispose();
  });

  rootElement.focus();
  await compose({editor, rootElement}, korean(['ㅎ', '하', '한']));

  editor.read(() => {
    expect($getRoot().getTextContent()).toBe('한');
  });
});
```

For a complete working setup with vitest browser mode configured from scratch, see the [`website-toolbar` example](https://github.com/facebook/lexical/tree/main/examples/website-toolbar).

## End-to-end (E2E) tests

E2E tests use [Playwright](https://playwright.dev/) to drive the full playground application in Chromium, Firefox, and WebKit. They click, type, and verify what appears on screen, with no access to Lexical internals. Install the browsers first:

```bash
pnpm exec playwright install
```

Then start the playground and run the tests:

```bash
pnpm start &
pnpm test-e2e-chromium   # or -firefox, -webkit
```

Tests live under `packages/lexical-playground/__tests__/` in `e2e/` (feature coverage) and `regression/` (one test per reported bug) directories.

## When to use which tier

| What you're testing | Tier |
|---|---|
| Node creation, transforms, serialization | Unit (jsdom) |
| A node property's whole domain, not one example | Unit (jsdom), property-based |
| Command dispatch and handler logic | Unit (jsdom) |
| Extension registration and config | Unit (jsdom) |
| Real keystroke input and typing | Browser |
| IME composition (Korean, Japanese, Chinese) | Browser |
| Selection from user interaction | Browser |
| Clipboard paste with real `DataTransfer` | Browser |
| Full-application user scenarios | E2E |
| Cross-browser rendering differences | E2E |

## Running specific tests

Pass a filename filter to run a subset of tests:

```bash
pnpm test-unit LexicalReconciler
pnpm test-browser compose
```

vitest matches the filter against file paths, so any substring of the filename works.

## General guidelines

- New features must include tests.
- If it can break, it should have a test.
- Do not merge pull requests with failing tests — this blocks other people and releases.

---

### Packages Lexical Website Docs Collaboration Faq (`packages/lexical-website/docs/collaboration/faq.md`)

---
sidebar_position: 2
---

# Collaboration FAQ

## Source of truth: Lexical State, Yjs and App's DB

It's recommended to treat the Yjs model as the source of truth. You can store the document to a database for indexing.
But, if possible, you should never forget the Yjs model, as this is the only way clients without internet access can reliably join and sync with the server.

You can also treat the database as the source of truth. This is how it could be achieved:

- Clients receive a `sessionId` when they connect to the server
- When a client connects without an existing `sessionId`, get the content from the database and create a `sessionId`
- When all clients disconnect, forget the room content and `sessionId` on the server after some timeout (e.g. 1 hour)
- When a client reconnects, use that content on the server. Furthermore, get the `sessionId` from the client
- When two clients with different `sessionId` reconnect, one of the clients should forget the room content. _In this case the client will lose content_ - although it is very unlikely if you set the forget timeout (see point 2) very high.

Or, there is an ever simpler approach:

- When a client connects to the server, the server populates the room content if empty
- When all clients disconnect, the server forgets the room content after some timeout (e.g. 1 hour)
- When a client was not able to reconnect for 40 minutes, the client must forget its local updates and start fresh (this should be enforced by the server)

When the database is the source of truth, and if you want to be able to forget the Yjs model, you will always run into cases where clients are not able to commit changes. That's not too bad in most projects. It somehow limits you, because you can't cache the document on the client using y-indexeddb. On the other hand, it is much easier to maintain, and do Yjs upgrades. Furthermore, most people would say that SQL is a bit more reliable than Yjs.

_* Based on the advice of the Yjs author - [Kevin Jahns](https://github.com/yjs/yjs/issues/82#issuecomment-328365015)_


## Initializing `EditorState` from Yjs Document

It's achievable by leveraging headless Lexical and no-op provider for Yjs:

<details>
  <summary>createHeadlessCollaborativeEditor.ts</summary>

  ```
/* Detailed source-code truncated for AI context efficiency. */
```
</details>

---

### Packages Lexical Website Docs Collaboration React (`packages/lexical-website/docs/collaboration/react.md`)

---
sidebar_position: 1
---

# React

Lexical provides `LexicalCollaborationPlugin` and `useCollaborationContext` hook within `@lexical/react` to accelerate creation of the collaborative React backed editors.
This is on top of the Yjs bindings provided by `@lexical/yjs`.


:::tip

Clone [Lexical GitHub repo](https://github.com/facebook/lexical), run `pnpm i && pnpm run start` and open [`http://localhost:3000/split/?isCollab=true`](http://localhost:3000/split/?isCollab=true) to launch playground in collaborative mode.

:::

## Getting started

This guide is based on [examples/react-rich](https://github.com/facebook/lexical/tree/main/examples/react-rich) example.

**Install minimal set of the required dependencies:**
```bash
$ npm i -S @lexical/react @lexical/yjs lexical react react-dom y-websocket yjs
```

:::note

`y-websocket` is the only officially supported Yjs connection provider at this point. Although other providers may work just fine.

:::

**Get WebSocket server running:**

This allows different browser windows and different browsers to find each other and sync Lexical state. On top of this `YPERSISTENCE` allows you to save Yjs documents in between server restarts so clients can simply reconnect and keep editing.

```bash
$ HOST=localhost PORT=1234 YPERSISTENCE=./yjs-wss-db npx y-websocket
```

**Get basic collaborative Lexical setup:**

```
/* Detailed source-code truncated for AI context efficiency. */
```

**Initial editor content:**

In a production environment, you should bootstrap the editor's initial content on the server. If bootstrapping was left to the client and two clients connected at the same time, they could both try to initialize the content resulting in document corruption.

Using the `withHeadlessCollaborationEditor` function from the [FAQ](faq.md) page, you can create a bootstrapped `Y.Doc` with the following:

```tsx
import type {CreateEditorArgs} from 'lexical';

import {$getRoot, $createParagraphNode} from 'lexical';
import {Doc} from 'yjs';

import {withHeadlessCollaborationEditor} from './createHeadlessCollaborativeEditor';

function createBootstrappedYDoc(nodes: CreateEditorArgs['nodes']): Doc {
  return withHeadlessCollaborationEditor(nodes, (editor, binding) => {
    editor.update(() => {
      $getRoot().append($createParagraphNode());
    }, {discrete: true});
    // binding.doc is the Doc the editor synced into
    return binding.doc;
  });
}
```

If you're simply following the above example to play around in a local dev environment, then you can add the following props to `CollaborationPlugin` to initialize the editor state client-side:

```tsx
// Dev-testing only, do not use in real-world cases.
initialEditorState={$initialEditorState}
shouldBootstrap={true}
```

## See it in action

Source code: [examples/react-rich-collab](https://github.com/facebook/lexical/tree/main/examples/react-rich-collab)

<iframe width="100%" height="600" src="https://stackblitz.com/github/facebook/lexical/tree/main/examples/react-rich-collab?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview" sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"></iframe>

## Sharing one Yjs document between editors

By default a binding uses the `XmlText` shared type named `root` on the Yjs
`Doc`, so each editor needs its own document. Two options let several editors
live in one document instead, which is what applications like collaborative
sticky notes or comment threads usually want:

- `rootName` picks a different top-level shared type, one per editor:

```jsx
<CollaborationPlugin
  id={`note-${noteId}`}
  providerFactory={providerFactory}
  rootName={`note-${noteId}`}
  shouldBootstrap={true}
/>
```

- `getXmlText` resolves the root yourself, for roots that are not top-level
  shared types (for example an `XmlText` stored in a `Y.Map`). It takes
  precedence over `rootName`:

```jsx
const getXmlText = useCallback(
  (doc) => doc.getMap('notes').get(noteId),
  [noteId],
);

<CollaborationPlugin
  id={`note-${noteId}`}
  providerFactory={providerFactory}
  getXmlText={getXmlText}
  shouldBootstrap={true}
/>
```

Give each editor its own `id`, as above: the `id` keys the document map and the
remote-cursor highlight registry, both of which are shared across the page, so
two editors mounted under one `id` overwrite each other's entries.

Both options are also available on `createYjsBinding` for non-React usage.

:::warning

The root is chosen when the binding is created, and a mounted editor cannot be
repointed at another root: changing `rootName` or `getXmlText` afterwards has no
effect. To switch documents, remount the **editor** — put a `key` that changes
with the document on the component that owns the `LexicalComposer`. Remounting
only the plugin is not enough: the editor would keep the previous document's
content and write it into the newly selected root.

:::

`getXmlText` runs while the binding is constructed, before the provider has
synced, so it has to resolve the root from whatever the local `Doc` holds at
that moment — reaching into a `Y.Array` that the server has not sent yet throws.
Either mount the editor only once the note exists locally, or have the callback
create the missing shared type. Write it to be idempotent: it can run more than
once for one editor (React StrictMode double-invokes render, and a remount
builds a new binding), and a callback that unconditionally stores a new
`XmlText` would throw away what the previous call stored.

The returned `XmlText` is checked to be an `XmlText` that is integrated into the
document; that it is not already driving another live binding is yours to
guarantee, since a second binding over one root takes over the collab nodes
cached on its shared types.

The experimental `CollaborationPluginV2__EXPERIMENTAL` (and
`createBindingV2__EXPERIMENTAL`) take the same two options, except that its
root is an `XmlElement`, so the callback is named `getXmlElement`. Create that
element as `new Y.XmlElement()` without a `nodeName`, which is how the root is
recognized.

## Building collaborative plugins

[Lexical Playground](https://playground.lexical.dev/) features set of the collaboration enabled plugins that integrate with primary document via `useCollaborationContext()` hook. Notable mentions:

- [`CommentPlugin`](https://github.com/facebook/lexical/tree/main/packages/lexical-playground/src/plugins/CommentPlugin) - features use of the separate provider and Yjs room to sync comments.
- [`ImageComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/ImageComponent.tsx) - features use of the `LexicalExtensionEditorComposer` for its nested caption editor.
- [`PollOptionComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/PollComponent.tsx) - showcases poll implementation using `name` from the collaboration context.
- [`StickyComponent`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/StickyComponent.tsx) - features use of the `LexicalExtensionEditorComposer` for its nested editor as well as sticky note position real-time sync.

:::note

While these "playground" plugins aren't production ready - they serve as a great example of collaborative Lexical capabilities as well as provide a good starting point.

:::

## Custom node property syncing

`@lexical/yjs` syncs custom node properties by constructing a fresh node instance and inspecting its **own enumerable properties**. This means every property that should be synced across peers must be assigned in the constructor, even if the initial value is `undefined`.

:::warning

Declaring a class property with TypeScript's optional syntax (`foo?: Type`) does **not** guarantee the property exists as an own enumerable property on the instance. Under TypeScript's default compilation settings (target < ES2022 without `useDefineForClassFields`), such declarations produce no initialization code.

:::

### What to do

Always initialize every node property in the constructor. Conditional initialization such as `if (someValue !== undefined) this.__someValue = someValue` is not enough, because the default construction path still needs to create the property.

For example:

```ts
// ✅ Correct — property is guaranteed to be an own property
class MyNode extends ElementNode {
  __someValue: string | undefined;

  constructor(someValue?: string, key?: NodeKey) {
    super(key);
    this.__someValue = someValue; // explicitly initialized
  }
}

// ❌ Incorrect — property may not exist as an own property
class MyNode extends ElementNode {
  __someValue?: string; // optional syntax without initialization

  constructor(key?: NodeKey) {
    super(key);
    // __someValue is never assigned → won't be synced via yjs
  }
}
```

If you use [`NodeState`](../concepts/node-state.md) for your custom properties, this concern does not apply. `NodeState` values are synced correctly without relying on constructor-created enumerable properties.

## Yjs providers

Setting up the communication between clients, managing awareness information, and storing shared data for offline usage is quite a hassle. Providers manage all that for you and are the perfect starting point for your collaborative app.

See [Yjs Website](https://docs.yjs.dev/ecosystem/connection-provider) for the list of the officially endorsed providers. Although it's not an exhaustive one.

---

### Packages Lexical Website Docs Concepts Commands (`packages/lexical-website/docs/concepts/commands.md`)

# Commands

Commands are a very powerful feature of Lexical that lets you register listeners for events like `KEY_ENTER_COMMAND` or `KEY_TAB_COMMAND` and contextually react to them _wherever_ & _however_ you'd like.

This pattern is useful for building [`Toolbars`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx) or complex extensions and nodes such as [tables](https://github.com/facebook/lexical/tree/main/packages/lexical-table) which require special handling for `selection`, `keyboard events`, and more.

When registering a `command` you supply a `priority` and can return `true` to mark it as "handled", which stops other listeners from receiving the event. If a command isn't handled explicitly by you, it's likely handled by default in [`RichTextExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-rich-text/src/index.ts) or [`PlainTextExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-plain-text/src/index.ts).

## `createCommand(...)`

You can view the core commands in the [`lexical` API reference](/docs/api/modules/lexical#commands). Other packages define their own, listed under Commands in each package's API reference, such as [`TOGGLE_LINK_COMMAND`](/docs/api/modules/lexical_link#toggle_link_command) in `@lexical/link`. If you need a custom command for your own use case, check out the typed `createCommand(...)` function.

```js
const HELLO_WORLD_COMMAND: LexicalCommand<string> = createCommand('HELLO_WORLD');

editor.registerCommand(
  HELLO_WORLD_COMMAND,
  (payload: string) => {
    console.log(payload); // Hello World!
    return false;
  },
  COMMAND_PRIORITY_EDITOR,
);

editor.dispatchCommand(HELLO_WORLD_COMMAND, 'Hello World!');
```

## `editor.dispatchCommand(...)`

Commands can be dispatched from anywhere you have access to the `editor` such as a Toolbar Button, an event listener, or an extension, but most of the core commands are dispatched from [`LexicalEvents.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/LexicalEvents.ts).

Calling `dispatchCommand` will implicitly call `editor.update` to trigger its command listeners if it was not called from inside `editor.update`.

Don't dispatch a command from inside a read-only context such as
`editor.read(fn)` or `editorState.read(fn)`. Command listeners usually change
the editor, so Lexical runs them in a separate writable update and
development builds log a warning when they detect this. Dispatch after the
read returns instead.

```js
editor.dispatchCommand(command, payload);
```

The `payload`s are typed via the `createCommand(...)` API, but they're usually a DOM `event` for commands dispatched from an event listener.

Here are some real examples from [`LexicalEvents.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/LexicalEvents.ts).

```js
editor.dispatchCommand(KEY_ARROW_LEFT_COMMAND, event);
// ...
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
```

And another example from the [`ToolbarPlugin`](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/ToolbarPlugin/index.tsx) in our Playground.

```js
const formatBulletList = () => {
  editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);
};
```

Which is later handled by [`ListExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-list/src/LexicalListExtension.ts) to insert the list into the editor.

```js
editor.registerCommand(
  INSERT_UNORDERED_LIST_COMMAND,
  () => {
    $insertList('bullet');
    return true;
  },
  COMMAND_PRIORITY_LOW,
);
```

## `editor.registerCommand(...)`

You can register a command from anywhere you have access to the `editor` object, but it's important that you remember to clean up the listener with its remove listener callback when it's no longer needed.

The command listener will always be called from an `editor.update`, so you may use dollar functions. You should not use
`editor.update` (and *never* call `editor.read(...)` or `editor.read('force-commit', ...)`) synchronously from within a command listener. It is safe to call
`editor.read('latest', ...)` or `editor.getEditorState().read` if you need to read the previous state after updates have already been made.

```js
const removeListener = editor.registerCommand(
  COMMAND,
  (payload) => boolean, // Return true to stop propagation.
  priority,
);
// ...
removeListener(); // Cleans up the listener.
```

The easiest way to get clean-up right is to register commands from an
[extension](/docs/extensions/defining-extensions)'s `register` function and
return the remove callback. The editor calls it when it is disposed. To
register several things, combine their callbacks with `mergeRegister` from
`@lexical/utils`.

```js
import {LinkExtension, TOGGLE_LINK_COMMAND} from '@lexical/link';
import {COMMAND_PRIORITY_BEFORE_EDITOR, defineExtension} from 'lexical';

export const LinkLoggerExtension = defineExtension({
  // The command it observes is handled by LinkExtension
  dependencies: [LinkExtension],
  name: '@my-app/LinkLogger',
  register: (editor) =>
    editor.registerCommand(
      TOGGLE_LINK_COMMAND,
      (payload) => {
        console.log('link', payload);
        return false; // let the link extension handle it too
      },
      // LinkExtension handles this command at COMMAND_PRIORITY_EDITOR and
      // returns true, so observe it at the BEFORE variant of that priority.
      COMMAND_PRIORITY_BEFORE_EDITOR,
    ),
});
```

A listener that only observes a command, like this one, needs to run before
the listener that handles it. Depending on the extension that handles it and
using the `COMMAND_PRIORITY_BEFORE_*` variant of its priority does that
without escalating to a higher priority. See
[Priorities and ordering](#priorities-and-ordering) below.

In a legacy React plugin, return the `registerCommand` call from a
`useEffect` instead.

And as seen above and below, `registerCommand`'s callback can return `true` to signal to the other listeners that the command has been handled and propagation will be stopped.

Here's a simplified example of handling a `KEY_TAB_COMMAND` from [`TabIndentationExtension`](https://github.com/facebook/lexical/blob/main/packages/lexical-extension/src/TabIndentationExtension.ts), which is used to dispatch a `OUTDENT_CONTENT_COMMAND` or `INDENT_CONTENT_COMMAND`.

```js
editor.registerCommand(
  KEY_TAB_COMMAND,
  (payload) => {
    const event: KeyboardEvent = payload;
    event.preventDefault();
    return editor.dispatchCommand(
      event.shiftKey ? OUTDENT_CONTENT_COMMAND : INDENT_CONTENT_COMMAND,
    );
  },
  COMMAND_PRIORITY_EDITOR,
);
```

Note that the same `KEY_TAB_COMMAND` command is registered by [`LexicalTableSelectionHelpers.ts`](https://github.com/facebook/lexical/blob/main/packages/lexical-table/src/LexicalTableSelectionHelpers.ts), which handles moving focus to the next or previous cell within a `TableNode`, but the priority is high (`COMMAND_PRIORITY_HIGH`) because this behavior is very important.

### Priorities and ordering

Command listeners are called in the following order until a listener returns `true`:

- From priority highest to lowest (critical, high, normal, low, editor)
- All `COMMAND_PRIORITY_BEFORE_${priority}` listeners, most recently registered first
- All `COMMAND_PRIORITY_${priority}` listeners, in registration order

:::note

The `COMMAND_PRIORITY_BEFORE_*` priorities make it much easier to override
default behavior without escalating the priority.

:::

It is best practice to use the lowest priority possible, so most commands will
be registered with `COMMAND_PRIORITY_EDITOR` for the default behavior, then
commands to override that behavior can be registered with
`COMMAND_PRIORITY_BEFORE_EDITOR`. The higher priorities mostly serve purposes
such as being able to observe-but-not-handle events and to support legacy code
that predates the availability of `COMMAND_PRIORITY_BEFORE_*`.

A modern lexical app will typically only need the
`COMMAND_PRIORITY_BEFORE_EDITOR` priority since the
last-registered-called-first ordering is suitable for almost all use cases. The
older priorities without BEFORE can be considered legacy and are primarily
offered for compatibility.

Here is the full ordering of priorities, from lowest to highest:

- `COMMAND_PRIORITY_EDITOR`
- `COMMAND_PRIORITY_BEFORE_EDITOR`
- `COMMAND_PRIORITY_LOW`
- `COMMAND_PRIORITY_BEFORE_LOW`
- `COMMAND_PRIORITY_NORMAL`
- `COMMAND_PRIORITY_BEFORE_NORMAL`
- `COMMAND_PRIORITY_HIGH`
- `COMMAND_PRIORITY_BEFORE_HIGH`
- `COMMAND_PRIORITY_CRITICAL`
- `COMMAND_PRIORITY_BEFORE_CRITICAL`

---

### Packages Lexical Website Docs Concepts Compared With Prosemirror (`packages/lexical-website/docs/concepts/compared-with-prosemirror.md`)

# Compared with ProseMirror

Lexical and [ProseMirror](https://prosemirror.net) are both frameworks for
building rich text editors on top of `contentEditable`, and both keep an
immutable document model as the source of truth. This page collects the main
differences in one place, for readers who know one and are learning the other.

## Document model {#document-model}

Lexical's tree is shaped like the HTML it renders (see
[Document Model](./document-model.md)). This is a deliberate difference from
ProseMirror. ProseMirror's document is also a tree,
and blocks can contain other blocks, but the inline content of a textblock is
a flat sequence of nodes (text and inline nodes) with marks for formatting and
links, and every position in the document is a single integer.

| | Lexical | ProseMirror |
| -- | -- | -- |
| Document | Immutable tree of nodes, one `RootNode` | Immutable tree of nodes, one `doc` node |
| Node identity and navigation | Each node has a runtime key and knows its parent and siblings | Nodes are plain values with no parent links; a resolved position supplies ancestor context |
| Blocks | `ElementNode` subclasses (paragraph, heading, list, table, …), or a block `DecoratorNode` (image, embed, …) | Block nodes defined in the schema |
| Inline formatting | Format flags on each `TextNode` (bold, italic, code, …) | Marks on text |
| Links and other inline wrappers | Inline `ElementNode`s (`LinkNode`, `MarkNode`) that contain text nodes | Marks on text, like formatting |
| Embedded content | `DecoratorNode`, inline or block, rendered by your framework (for example React) | Leaf or atom nodes, often with a custom `NodeView` |
| Several editable regions in one node | [Named slots](./named-slots.md): regions addressed by name, like a card's `title`, each isolated so editing and selection never cross the boundary | Child nodes in the order the schema's content expression allows, optionally marked `isolating`, or a separate editor inside a `NodeView` |
| Addressing a position | Node key plus offset (`{key, offset, type}`) | One integer counted across the whole document |
| Allowed structure | Declared by node classes, enforced with node transforms and normalization | Declared by a schema of content expressions |
| Applying edits | Call node and selection methods inside `editor.update()` | Build a transaction from steps that address positions or ranges |
| Rendering | Each node's `createDOM()`/`updateDOM()`, applied by the reconciler, and optionally changed per node class by a `DOMRenderExtension` override | `toDOM` in the schema, or a `NodeView` |

## Serialization {#serialization}

Because a Lexical link is an element that already contains its text, it
exports as an `<a>` around those text nodes. A mark-based model has to find the
adjacent text that shares a mark and group it into one element first.

| Format | Lexical | ProseMirror |
| -- | -- | -- |
| JSON | `editorState.toJSON()`, restored with `editor.parseEditorState()` | `doc.toJSON()` and `Node.fromJSON(schema, json)` |
| HTML export | `$generateHtmlFromNodes()`, using each node's `createDOM()` or `exportDOM()` | `DOMSerializer.fromSchema(schema)`, using `toDOM` from the schema's nodes and marks |
| HTML import | `$generateNodesFromDOM()`, using `importDOM()` or `DOMImportExtension` rules | `DOMParser.fromSchema(schema)`, using the schema's `parseDOM` rules |
| Markdown | `@lexical/markdown` transformers or `@lexical/mdast` | `prosemirror-markdown`: a markdown-it parser and a serializer with a function per node and mark |
| Clipboard | Copy writes plain text, HTML, and Lexical JSON. Paste prefers the JSON, then HTML, then plain text. | Copy writes HTML and plain text. Paste parses the HTML with the schema's parse rules. |

In the JSON, a Lexical link is a `link` node with its text nodes as
`children`, and bold is a `format` value on a `text` node, where
ProseMirror's JSON gives each text node a list of `marks`. In both editors the
default HTML export reuses the definition that renders the editor
(`createDOM()` in Lexical, `toDOM` in ProseMirror), so the two stay in step
unless you override one. See [Serialization](../serialization/serialization.md)
for the details of each Lexical format.

## History {#history}

Both editors group consecutive edits into one undo step. Lexical's
[`HistoryExtension`](./history.md) merges edits made within `delay`
(300ms by default) and keeps an unlimited stack unless you set `maxDepth`.
ProseMirror's history plugin uses `newGroupDelay: 500` and `depth: 100` by
default, so `configExtension(HistoryExtension, {delay: 500, maxDepth: 100})`
matches its behavior.

---

### Packages Lexical Website Docs Concepts Decorators (`packages/lexical-website/docs/concepts/decorators.mdx`)

# Decorators

Decorator nodes are used when a piece of editor content needs a custom view
instead of a text-only or element-only DOM representation. Common examples are
media embeds, mentions, cards, horizontal rules, or interactive controls that
need application UI inside the editor.

A `DecoratorNode` still belongs to the `EditorState`, but its visible UI comes
from `decorate()`. The DOM element returned by `createDOM()` is the host that
Lexical reconciles. The decorated value returned by `decorate()` is rendered
into that host by the framework integration, such as React's decorators in
`@lexical/react`.

## When to use a decorator

Use a decorator node when:

- The content is represented by structured data rather than editable text.
- The rendered UI is owned by your application or framework.
- Selection should treat the content as a single node or custom interactive
  region.
- The node needs a separate DOM import, DOM export, or JSON serialization shape.

Prefer `TextNode` or `ElementNode` when the content can be edited directly as
normal text or nested editor content. For example, a custom paragraph style is
usually an `ElementNode`, while a video embed is a decorator.

## Inline and block decorators

`DecoratorNode` is inline by default. Override `isInline()` to return `false`
for block-level content such as horizontal rules, video embeds, or cards.

```ts
class VideoNode extends DecoratorNode<ReactNode> {
  isInline(): false {
    return false;
  }

  createDOM(): HTMLElement {
    return document.createElement('div');
  }

  updateDOM(): false {
    return false;
  }

  decorate(): ReactNode {
    return <VideoPlayer />;
  }
}
```

Block decorators often also need custom selection behavior. The built-in
`HorizontalRuleNode` is a useful reference because it is a block decorator that
registers click handling and node selection support.

## Registering decorators

Keep a decorator's node registration and behavior in one extension instead of
splitting them across an editor config and a framework plug-in. The extension
can provide the node and register commands, transforms, or listeners that
belong to it:

```ts
export const INSERT_VIDEO_COMMAND = /* @__PURE__ */ createCommand<string>(
  'INSERT_VIDEO_COMMAND',
);

export const VideoExtension = /* @__PURE__ */ defineExtension({
  name: 'example/Video',
  nodes: () => [VideoNode],
  register(editor) {
    return editor.registerCommand(
      INSERT_VIDEO_COMMAND,
      videoID => {
        $insertNodes([$createVideoNode(videoID)]);
        return true;
      },
      COMMAND_PRIORITY_EDITOR,
    );
  },
});
```

Add that extension to the root editor extension. React applications mount the
result with
[`LexicalExtensionComposer`](/docs/api/modules/lexical_react_LexicalExtensionComposer#lexicalextensioncomposer):

```tsx
const editorExtension = /* @__PURE__ */ defineExtension({
  name: 'ExampleEditor',
  namespace: 'Example',
  dependencies: [VideoExtension],
});

<LexicalExtensionComposer extension={editorExtension}>
  {/* toolbar and other editor UI */}
</LexicalExtensionComposer>;
```

The command can then be dispatched from a toolbar, menu, or any other UI:

```ts
editor.dispatchCommand(INSERT_VIDEO_COMMAND, videoID);
```

This keeps the configuration and registration together. A framework-independent
decorator whose `decorate()` returns `null` can use the same extension shape with
[`buildEditorFromExtensions`](/docs/api/modules/lexical_extension#buildeditorfromextensions)
outside React.

## State and serialization

Store only serializable data on the node or in [NodeState](/docs/concepts/node-state).
The decorated UI should be derived from that data. Do not store React elements,
DOM nodes, functions, `Map`, `Set`, or other non-serializable values in node
state.

The example below stores a video id with NodeState and renders a React
component from that id:

```tsx
const videoIDState = createState('videoID', {
  parse: value => (typeof value === 'string' ? value : ''),
});

class VideoNode extends DecoratorNode<ReactNode> {
  $config() {
    return this.config('video', {
      extends: DecoratorNode,
      stateConfigs: [{flat: true, stateConfig: videoIDState}],
    });
  }

  createDOM(): HTMLElement {
    return document.createElement('div');
  }

  updateDOM(): false {
    return false;
  }

  decorate(): ReactNode {
    return <VideoPlayer videoID={$getState(this, videoIDState)} />;
  }
}

export function $createVideoNode(videoID: string): VideoNode {
  return $setState($create(VideoNode), videoIDState, videoID);
}
```

NodeState can hold richer serializable shapes as long as the state config
knows how to parse them. Use a
[`StateValueConfig`](/docs/api/modules/lexical#statevalueconfig) when a
decorator needs structured data such as ids, dimensions, captions, or embed
metadata.

If the node needs to survive copy and paste outside Lexical, implement
`exportDOM()` in addition to JSON serialization. For importing HTML, prefer
[`DOMImportExtension`](/docs/serialization/dom-import) for new code.

Static `importDOM()` remains the legacy alternative, but it is a separate
import mechanism and cannot be combined with `DOMImportExtension`. Choose one
pipeline; new extension-based editors should contribute their rules through
`DOMImportExtension`.

## Rendering model

`createDOM()` should create the stable host element for the node. `updateDOM()`
returns whether Lexical should replace that host when the node changes. Most
decorator nodes return `false` because the host can stay in place while the
decorated value changes.

`decorate()` returns the framework-specific rendered value. In React, the
React plugin renders this value with a portal into the host element created by
`createDOM()`. Non-React integrations can use the same node data and provide
their own rendering layer.

Keep editor data and UI state separate. The node should store the content that
belongs in the editor state, while transient UI details such as hover state,
loading state, or local component state should live inside the decorated
component.

Decorators do not have to be tied to a UI framework. A `DecoratorNode` can
return `null` from `decorate()` and do its visible work in `createDOM()` and
`updateDOM()`, or delegate behavior to extension hooks such as
[`DOMRenderExtension`](/docs/serialization/dom-render) or a mutation listener.
`HorizontalRuleNode` from `@lexical/extension` is an example of a decorator
node whose behavior can be managed without rendering a React component from
`decorate()`.

Decorator nodes also combine well with
[named slots](/docs/concepts/named-slots) when surrounding
application UI needs to be mounted in predictable places around the editor. For
advanced DOM ownership, `DOMSlot` and `ElementNode` patterns let an extension
place arbitrary DOM inside or around lexical content while still letting
Lexical manage the actual editor children. Use those patterns when the content
inside the custom UI should remain editable by Lexical. The playground's
`ReviewExtension` and `ReviewNode` on main are a useful example: the node uses
`getDOMSlot()` for its editable body, while the extension tracks live review
nodes with a mutation listener and renders the surrounding review UI. Use these
patterns only when a plain `ElementNode`, framework decorator, or portal would
not give enough control.

---

### Packages Lexical Website Docs Concepts Document Model (`packages/lexical-website/docs/concepts/document-model.md`)

# Document Model

An [editor state](./editor-state.md) holds a tree of nodes. This page explains
how that tree is shaped.

## A DOM-like tree

Lexical's node tree is shaped like the HTML it renders. A paragraph is an
`ElementNode` whose children are the nodes inside it, and inline structure
nests the same way: a link is a `LinkNode` element that contains the text
nodes it wraps, just as an `<a>` contains its text. Each node type decides how
it renders with `createDOM()` and `updateDOM()`, so the model maps closely to
the DOM, and a position in the document is a node plus an offset within it.

Here is a small document with a heading, a paragraph containing bold text and
a link, a block image, and a card that has a named `title` slot as well as
ordinary body children:

```mermaid
flowchart TB
  accTitle: The shape of a Lexical document tree
  accDescr: The root has four children in order: a heading, a paragraph, an image, and a card. The heading contains the text "Welcome". The paragraph contains the text "Read the ", a bold text node "docs", and a link, which contains the text "here". The image is a block decorator node with no children. The card has a named slot called title, which holds a paragraph containing the text "Card title", and one ordinary child, a paragraph containing the text "Card body".
  root["Root"] --> heading["Heading (h1)"]
  root --> paragraph["Paragraph"]
  root --> image["Image<br/>(block DecoratorNode)"]
  root --> card["Card<br/>(ElementNode)"]
  heading --> welcome["Text: 'Welcome'"]
  paragraph --> read["Text: 'Read the '"]
  paragraph --> docs["Text: 'docs'<br/>{ format: bold }"]
  paragraph --> link["Link"]
  link --> here["Text: 'here'"]
  card -.->|"slot: title"| titlePara["Paragraph"]
  titlePara --> titleText["Text: 'Card title'"]
  card --> body["Paragraph"]
  body --> bodyText["Text: 'Card body'"]
```

Solid arrows are ordinary children, which form an ordered list under their
parent and are read with `getChildren()`. The dashed arrow is a
[named slot](./named-slots.md): a separate channel on the host, keyed by name
instead of position and read with `$getSlot(card, 'title')`. A slot's value
is the root of its own isolated region, so its `getParent()` is `null` and
`$getSlotHost()` leads back to the card. Any `ElementNode` or `DecoratorNode`
can host slots, so a decorator such as the image could also expose an
editable caption this way.

## Working with the tree

Lexical code navigates the document the way DOM code does, with methods like
`getParent()`, `getChildren()`, and `getNextSibling()`, and each node owns the
DOM it renders. The resemblance is structural, not one
node per HTML element: bold or italic text, for example, is a format on a
`TextNode`, not a separate `<strong>` or `<em>` node. See
[Nodes](./nodes.mdx) for the built-in node types.

## Customizing rendering

You can also change how nodes render without subclassing them.
[`DOMRenderExtension`](../serialization/dom-render.md) from `@lexical/html`
takes overrides for `createDOM`, `updateDOM`, and `exportDOM`, for one node
class or for every node, such as adding a `data-` attribute or wrapping a
node's children in another element. Each override calls `$next()` to get the
default result and adjusts it, so overrides from several extensions compose.
Export overrides only affect HTML export, and `createDOM` overrides also carry
through to export for nodes that use the default `exportDOM`. Changes made
while updating the editor's DOM don't; see
[Lexical -> HTML](../serialization/serialization.md#lexical---html) for which
hooks reach export.

For how the tree is converted to and from JSON, HTML, and Markdown, see
[Serialization](../serialization/serialization.md#formats-at-a-glance). For
how this model differs from a mark-based editor, see
[Compared with ProseMirror](./compared-with-prosemirror.md).

---

### Packages Lexical Website Docs Concepts Dom Events (`packages/lexical-website/docs/concepts/dom-events.md`)

# Working with DOM Events

Sometimes, when working with Lexical, it might be necessary or useful for you to attach a DOM Event Listener to the underlying DOM nodes that Lexical controls. For instance, you might want to show a popover when a user mouses over a specific node or open a modal when they click on a node. Either of these use cases (and many others) can be accomplished via native DOM Event Listeners. There are 3 main ways that you can listen for DOM Events on nodes controlled by Lexical:

## 1. Event Delegation

One way to handle events inside the editor is to set a listener on the editor root element (the contentEditable Lexical attaches to). You can do this using a [Root Listener](listeners.md).

```js
function myListener(event) {
    // You may want to filter on the event target here
    // to only include clicks on certain types of DOM Nodes.
    alert('Nice!');
}

const ClickExtension = defineExtension({
  name: '@my-app/Click',
  register: (editor) =>
    editor.registerRootListener((rootElement) => {
      if (rootElement === null) {
        return;
      }
      // add the listener to the current root element
      rootElement.addEventListener('click', myListener);
      // remove the listener when the root element changes - make sure the ref
      // to myListener is stable so the removal works and you avoid a memory leak.
      return () => rootElement.removeEventListener('click', myListener);
    }),
});
```

An extension's `register` returns a cleanup function, and the editor calls it
when it is disposed, so there is no teardown to write by hand. Add
`ClickExtension` to your editor's `dependencies` to use it.
This can be a simple, efficient way to handle some use cases, since it's not necessary to attach a listener to each DOM node individually.

The `addEventListener`/`removeEventListener` pairing above is common enough that the core `lexical` package exports a `registerEventListener(target, type, listener, options?)` helper. It attaches the listener and returns a dispose function that removes it, so the example above becomes:

```js
import {defineExtension, registerEventListener} from 'lexical';

const ClickExtension = defineExtension({
  name: '@my-app/Click',
  register: (editor) =>
    editor.registerRootListener((rootElement) =>
      // registerEventListener returns the matching removeEventListener cleanup,
      // so there's no need to write the teardown by hand.
      rootElement === null
        ? undefined
        : registerEventListener(rootElement, 'click', myListener),
    ),
});
```

It mirrors the `addEventListener` overloads (so the event type is strongly typed) and composes well with `mergeRegister` when you need to register several listeners at once.

To attach several listeners to the same target, `registerEventListeners(target, listeners, options?)` takes a `{type: listener}` map and returns a single dispose function that removes all of them. Each listener's event argument is still strongly typed per event type, and the optional `options` argument is shared across every listener:

```js
import {registerEventListeners} from 'lexical';

const removeListeners = registerEventListeners(
  rootElement,
  {
    click: onClick,
    keydown: onKeyDown, // receives a KeyboardEvent
  },
  {capture: true}, // shared by both listeners
);
```

Because `options` is shared, register a group that needs different options (such as a different `capture` flag) with a separate `registerEventListeners` call and combine the results with `mergeRegister`.

## 2. Directly Attach Handlers

In some cases, it may be better to attach an event handler directly to the underlying DOM node of each specific node. With this approach, you generally don't need to filter the event target in the handler, which can make it a bit simpler. It will also guarantee that your handler isn't running for events that you don't care about. This approach is implemented via a [Mutation Listener](listeners.md).

```js
const registeredElements: WeakSet<HTMLElement> = new WeakSet();
const removeMutationListener = editor.registerMutationListener(nodeType, (mutations) => {
    editor.getEditorState().read(() => {
        for (const [key, mutation] of mutations) {
            const element: null | HTMLElement = editor.getElementByKey(key);
            if (
                // Updated might be a move, so that might mean a new DOM element
                // is created. In this case, we need to add an event listener too.
                (mutation === 'created' || mutation === 'updated') &&
                element !== null &&
                !registeredElements.has(element)
            ) {
                registeredElements.add(element);
                element.addEventListener('click', (event: Event) => {
                    alert('Nice!');
                });
            }
        }
    });
});

// teardown the listener - return it from an extension's `register` so the
// editor removes it when it is disposed.
removeMutationListener();
```
Notice that here we don't worry about cleaning up, as Lexical will dereference the underlying DOM nodes and allow the JavaScript runtime garbage collector to clean up their listeners.

## 3. Use NodeEventPlugin

If you're using React, we've wrapped approach #2 up into a simple React plugin that you can render inside your composer to achieve the same effect, without worrying about the details:

```jsx
<LexicalExtensionComposer extension={appExtension}>
    <NodeEventPlugin
        nodeType={LinkNode}
        eventType={'click'}
        eventListener={(e: Event) => {
            alert('Nice!');
        }}
    />
</LexicalExtensionComposer>
```

If the editor lives inside a shadow root or an iframe, see
[Shadow DOM and iframes](./shadow-dom.md) for the helper to read
`event.target` through the shadow boundary — the browser retargets it to the
shadow host on listeners attached above the boundary, so a plain
`event.target` won't match a node inside the editor.

---

### Packages Lexical Website Docs Concepts Editor State (`packages/lexical-website/docs/concepts/editor-state.md`)

# Editor State

## Why is it necessary?

With Lexical, the source of truth is not the DOM, but rather an underlying state model
that Lexical maintains and associates with an editor instance.

While HTML is great for storing rich text content it's often "way too flexible" when it comes to text editing.
For example the following lines of content will produce equal outcome:

```html
<i><b>Lexical</b></i>
<i><b>Lex<b><b>ical</b></i>
<b><i>Lexical</i></b>
```

<details>
  <summary>See rendered version!</summary>
  <div>
    <i><b>Lexical</b></i>
    <i><b>Lex</b><b>ical</b></i>
    <b><i>Lexical</i></b>
  </div>
</details>

Of course, there are ways to normalize all these variants to a single canonical form, however this would require DOM manipulation and so re-rendering of the content. And to overcome this we can use Virtual DOM, or State.

On top of that it allows to decouple content structure from content formatting. Let's look at this example stored in HTML:

```html
<p>Why did the JavaScript developer go to the bar? <b>Because he couldn't handle his <i>Promise</i>s</b></p>
```

<figure class="text--center">
  <img src="/img/docs/state-formatting-html.drawio.svg" alt="Nested structure of the HTML state"/>
  <figcaption>Nested structure of the HTML state because of the formatting</figcaption>
</figure>

In contrast, Lexical decouples structure from formatting by offsetting this information to attributes. This allows us to have canonical document structure regardless of the order in which different styles were applied.

Here is the same content as a Lexical node tree, printed the way the
[tree view](/docs/getting-started/devtools) shows it (the numbers are node keys):

```text
root
  └ (2) paragraph
    ├ (3) text "Why did the JavaScript developer go to the bar? "
    ├ (4) text "Because he couldn't handle his " { format: bold }
    ├ (5) text "Promise" { format: bold, italic }
    └ (6) text "s" { format: bold }
```

The paragraph's children are a flat list of text nodes, and the formatting is a
property of each one, so there is only one way to represent this content.

## Understanding the Editor State

You can get the latest editor state from an editor by calling `editor.getEditorState()`.

Editor states have two phases:

- During an update they can be thought of as "mutable". See "Updating state" below to
  mutate an editor state.
- After an update, the editor state is then locked and deemed immutable from there on. This
  editor state can therefore be thought of as a "snapshot".

Editor states contain two core things:

- The editor node tree (starting from the root node). See
  [Document Model](./document-model.md) for how the tree is shaped.
- The editor [selection](./selection.md) (which can be null).

For example, "Hello world" with a link around "world" and the caret at the
end looks like this:

```mermaid
flowchart TB
  accTitle: A simple editor state
  accDescr: The root contains a paragraph with a "Hello " text node and a link. The link contains the "world" text node. The selection's anchor and focus both point to offset 5 in "world".
  subgraph state["EditorState"]
    direction TB
    root["Root"] --> paragraph["Paragraph"]
    paragraph --> hello["Text: 'Hello '"]
    paragraph --> link["Link"]
    link --> world["Text: 'world'"]
    selection["Selection"] -.->|"caret at offset 5"| world
  end
```

`editor.getEditorState()` returns the latest committed state. Its `toJSON()`
method serializes the document tree only; the selection and the runtime node
keys are not included. To restore saved content, pass the JSON to
`editor.parseEditorState()` and the result to `editor.setEditorState()`. The
editor must have the same node types registered. See
[Serialization](../serialization/serialization.md) for JSON, HTML, and
Markdown.

Here's an example of how you can initialize an editor with saved content and
then persist it. The saved JSON is passed as the editor's `$initialEditorState`:

```js
import {buildEditorFromExtensions} from '@lexical/extension';
import {RichTextExtension} from '@lexical/rich-text';
import {defineExtension} from 'lexical';

// Get the saved content (e.g. loaded from a backend)
const initialEditorState = await loadContent();

const editor = buildEditorFromExtensions(
  defineExtension({
    $initialEditorState: initialEditorState,
    dependencies: [RichTextExtension],
    name: '@my-app/editor',
  }),
);
editor.setRootElement(document.getElementById('editor'));

// Store the content (e.g. when the user submits a form)
async function onSubmit() {
  await saveContent(JSON.stringify(editor.getEditorState()));
}
```

With React, pass the extension to `LexicalExtensionComposer`. A component
rendered inside it can reach the editor with `useLexicalComposerContext()`:

```jsx
import {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {RichTextExtension} from '@lexical/rich-text';
import {defineExtension} from 'lexical';
import {useMemo} from 'react';

function SaveButton() {
  const [editor] = useLexicalComposerContext();
  return (
    <button
      onClick={() => saveContent(JSON.stringify(editor.getEditorState()))}>
      Save
    </button>
  );
}

function Editor({initialEditorState}) {
  // The editor is recreated whenever this extension changes,
  // so keep it stable for the life of the editor.
  const extension = useMemo(
    () =>
      defineExtension({
        $initialEditorState: initialEditorState,
        dependencies: [RichTextExtension],
        name: '@my-app/editor',
      }),
    [initialEditorState],
  );
  return (
    <LexicalExtensionComposer extension={extension}>
      <SaveButton />
    </LexicalExtensionComposer>
  );
}
```

`$initialEditorState` is applied once, when the editor is built; changing it
later has no effect on that editor. See "Updating state" below for the proper
way to change editor state after initialization.

`$initialEditorState` accepts:

- a JSON string or a parsed `SerializedEditorState` object, passed to
  `editor.parseEditorState()` (as in the example above);
- an `EditorState` instance, applied directly with `editor.setEditorState()`;
- a function `(editor) => void`, run inside `editor.update(...)`;
- `null`, which skips default initialization entirely. Use this with
  [collaboration](/docs/collaboration/react) so that the Yjs document, not
  Lexical, owns the initial state.

Omitting the field (or passing `undefined`) seeds the root with a default empty
`ParagraphNode`. The two are not interchangeable: `null` leaves the root with no children,
while `undefined` produces a single empty line. If your `loadContent` may yield `null` or
`undefined` for new documents, coalesce to `undefined` (e.g. `(await loadContent()) ?? undefined`)
so the editor still gets the default paragraph rather than the collab-style uninitialized
state.

The legacy `LexicalComposer` takes the same values as `initialConfig.editorState`,
except that a function there only runs if the root is still empty.

## Updating state

All reads and writes of the document happen inside a synchronous callback:

```js
import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';

editor.update(() => {
  const paragraph = $createParagraphNode();
  paragraph.append($createTextNode('Hello world'));
  $getRoot().append(paragraph);
});

const text = editor.read('force-commit', () => $getRoot().getTextContent());
```

### The `$` function convention {#dollar-functions}

Functions whose names start with `$`, such as `$getRoot()` and
`$getSelection()`, only work inside one of these callbacks, because they act
on the active editor state. Calling them anywhere else throws an error. The
convention is similar to React Hooks:

| | React Hooks | Lexical `$` functions |
| -- | -- | -- |
| Naming | `useFunction` | `$function` |
| Can only be called | while rendering a component | inside an update or read |
| Can call others of the same kind | ✅ | ✅ |
| Must be synchronous | ✅ | ✅ |
| Must be called unconditionally, in the same order | ✅ | ❌ No such rule |

Command handlers and node transforms already run inside an update, so they
can call `$` functions directly.

This works because, while a callback runs, Lexical keeps the active editor and
the active editor state in module-level variables, and `$` functions read
them: `$getRoot()` returns the root of the active state, and `$getEditor()`
returns the active editor. Because the callback is synchronous, nothing else
can run in between and change them. The same mechanism enforces the
difference between reads and updates. Inside a read, a method that would
change a node throws `Cannot use method in read-only mode.`

Give your own functions a `$` prefix when they call `$` functions, so it is
clear where they can be used. The
[`@lexical/rules-of-lexical`](/docs/packages/lexical-eslint-plugin) ESLint rule
checks this convention.

The same rule applies to node objects. Call node methods only inside a read or
update. Every node has a key that identifies it across versions of the editor
state, and node methods use that key to find the latest version of the node.
This is why a node reference taken earlier in an update stays usable after the
node changes. Keys exist only at runtime: they are not serialized, and you
should treat them as opaque.

Because the callbacks are synchronous, do any asynchronous work (fetching
data, awaiting a promise) first, and then enter an update with the result.

Avoid nesting one `editor.update()` inside another: the inner update does not
run immediately but is queued to run after the outer one. Never start an
update inside a read.

Each update works on a pending copy of the editor state, and updates made in
the same tick are committed and reconciled to the DOM together. See
[Updates](./updates.mdx#update-lifecycle) for the full lifecycle.

### Replacing the state

Another way to set state is `setEditorState` method, which replaces current state with the one passed as an argument.

Here's an example of how you can set editor state from a stringified JSON:

```js
const editorState = editor.parseEditorState(editorStateJSONString);
editor.setEditorState(editorState);
```

:::warning

`setEditorState` rejects an `EditorState` that satisfies `editorState.isEmpty()` — i.e.
the root is the only node and there is no selection. In a development build it throws
`"setEditorState: the editor state is empty. Ensure the editor state's root node never
becomes empty."`; in production it warns with the same message and recovers by appending
an empty `ParagraphNode`, so the editor stays usable. The `EditorState` produced by
initializing with `editorState: null` and never appending content (typical for a
collaboration document before peers connect) has exactly this shape, so persisting and
reloading such a state hits this path. Note that `parseEditorState` itself succeeds — the
check lands on the apply step. To avoid relying on the recovery, guard with
`editorState.isEmpty()` before calling `setEditorState`, or seed the document with a
`ParagraphNode` before serializing.

:::

## Reading state

Which state you see depends on how you read it:

- Inside `editor.update()` you see the **pending** state, where your changes
  are visible but transforms and reconciliation may not have run yet.
  `editor.read('pending', fn)` gives you the same view without allowing
  changes.
- `editor.read('force-commit', fn)` first commits any pending updates, so it
  always sees a consistent, reconciled state. Do not call it inside an update.
  `editor.read(fn)` with no mode does the same thing, and is kept for
  convenience and backwards compatibility. Inside a mutation or update
  listener it does not commit an update that another listener started (that
  update commits after the listeners have run, so they observe commits in
  order); use `editor.read('latest', fn)` there.
- `editor.read('latest', fn)` reads the most recently reconciled state without
  committing anything, so pending changes are not visible.
- `editorState.read(fn, {editor})` reads one particular snapshot, such as the
  `editorState` an update listener receives. Passing the `editor` keeps
  `$getEditor()` and extension lookups working inside the callback.

## State update listener

If you want to know when the editor updates so you can react to the changes, you can add an update
listener to the editor, as shown below:

```js
editor.registerUpdateListener(({editorState}) => {
  // The latest EditorState can be found as `editorState`.
  // To read the contents of the EditorState, use the following API:

  editorState.read(
    () => {
      // Just like editor.update(), .read() expects a closure where you can use
      // the $ prefixed helper functions.
    },
    // Passing the editor keeps $getEditor() and extension lookups working.
    {editor},
  );
});
```

## When are Listeners, Transforms, and Commands called?

There are several types of callbacks that can be registered with the editor that are related to
updates of the Editor State.

| Callback Type | When It's Called |
| -- | -- |
| Update Listener | After reconciliation |
| Mutation Listener | After reconciliation |
| Node Transform | During `editor.update()`, after the callback finishes, if any instances of the node type they are registered for were updated |
| Command | As soon as the command is dispatched to the editor (called from an implicit `editor.update()`) |

## Synchronous reconciliation with discrete updates

While commit scheduling and batching are normally what we want, they can sometimes get in the way.

Consider this example: you're trying to manipulate an editor state in a server context and then persist it in a database.

```js
editor.update(() => {
  // manipulate the state...
});

saveToDatabase(editor.getEditorState().toJSON());
```

This code will not work as expected, because the `saveToDatabase` call will happen before the state has been committed.
The state that will be saved will be the same one that existed before the update.

Fortunately, the `discrete` option for `LexicalEditor.update` forces an update to be immediately committed.

```js
editor.update(() => {
  // manipulate the state...
}, {discrete: true});

saveToDatabase(editor.getEditorState().toJSON());
```

### Cloning state

Lexical state can be cloned, optionally with custom selection. One of the scenarios where you'd want to do it
is setting editor's state but not forcing any selection:

```js
// Passing `null` as a selection value to prevent focusing the editor
editor.setEditorState(editorState.clone(null));
```

---

### Packages Lexical Website Docs Concepts Headless (`packages/lexical-website/docs/concepts/headless.md`)

# Running Without a Browser

The DOM is only needed to show an editor on screen. Because every read and
update goes through the editor state, Lexical also works where there is no
browser DOM:

- **With no DOM at all.** An editor that has no root element skips
  reconciliation and DOM selection entirely, so you can build one with
  `buildEditorFromExtensions()` and never call `setRootElement()`. Adding
  `HeadlessExtension` from `@lexical/headless` makes that explicit: attaching a
  root element then throws, and nested editors are headless too. Updates,
  transforms, listeners, commands, and JSON serialization all still work,
  which is enough to process documents on a server, apply changes from a
  collaboration backend, or write tests.
- **With a virtual DOM** such as happy-dom or jsdom, for the features that
  create DOM nodes, like HTML import and export. `withDOM()` from
  `@lexical/headless/dom` runs a callback with a temporary happy-dom window,
  for example to convert HTML on a server.

## Example: converting saved JSON to HTML on a server

```ts
import {buildEditorFromExtensions} from '@lexical/extension';
import {HeadlessExtension} from '@lexical/headless';
import {withDOM} from '@lexical/headless/dom';
import {$generateHtmlFromNodes} from '@lexical/html';
import {RichTextExtension} from '@lexical/rich-text';
import {defineExtension} from 'lexical';

const editor = buildEditorFromExtensions(
  defineExtension({
    dependencies: [HeadlessExtension, RichTextExtension],
    name: '@my-app/server-editor',
  }),
);

export function jsonToHtml(json: string): string {
  editor.setEditorState(editor.parseEditorState(json));
  return withDOM(() => editor.read(() => $generateHtmlFromNodes(editor)));
}
```

The editor must have the same node types registered as the editor that saved
the JSON, which is why it uses the same extensions. JSON and Markdown
conversion do not need `withDOM()`.

## `createHeadlessEditor()`

Before extensions, headless editors were created with `createHeadlessEditor()`
from `@lexical/headless`. It still works, and it makes DOM-only methods such as
`registerRootListener` and `registerMutationListener` throw. Because many
extensions register those listeners, use `HeadlessExtension` with
`buildEditorFromExtensions()` in new code.

---

### Packages Lexical Website Docs Concepts History (`packages/lexical-website/docs/concepts/history.md`)

# History

Lexical adds support for Undo and Redo through the `@lexical/history` package —
`registerHistory`, the `HistoryExtension` extension, or the `HistoryPlugin`
React component from `@lexical/react/LexicalHistoryPlugin`. Part of undo and redo requires supporting coalescing of certain dynamic operations such as continuous typing. The below provides background information regarding the continuous typing undo coalescing feature.

**Undo Coalescing of Typed Text**

**TL;DR**
When working with text editors, users expect a well polished undo experience. A key part of this is a smooth, well thought out workflow when undoing continuous typing events. For this post, strict typing events may be limited to typing characters, backspacing and forward deleting. Users generally expect that continuous typing should fully undo with a single undo gesture and that they don’t require separate undo gestures for each character.
The algorithm that covers this is “undo coalescing of typed text” and there’s a certain degree of nuance involved. This post covers that nuance while attempting to avoid prescriptive implementation details.

**What Kind of Edits Require Coalescing?**
As mentioned above, continuous typing should be coalesced into a single undo action. Continuous typing is a loaded phrase and we’ll get into some details below.
There are other types of edits that require undo coalescing such as rapidly resizing an image or rapidly changing the font size of selected text. These latter two examples fall under what might be termed “dynamic operations” and their implementation might vary quite a bit from coalescing of typing.

**What Constitutes Continuous Typing?**
For the purposes of undo coalescing, continuous typing events are safely limited to these three actions:

1. Forward typing characters (heads up, composed character typing discussed later).
2. Deleting backwards, e.g. backspace gestures including backspace key presses.
3. Forward deletion, e.g. forward delete gestures including Delete key presses.

Any interruption to the above breaks the notion of continuous typing. Note that switching between either of these three actions may itself be considered an interruption of continuous typing or it may not. This ends up being a usability decision. In my opinion, when examining various word processors, switching between these three actions should maintain continuity, because users often make typos or corrections in the flow of typing and therefore would lean towards the whole sequence of typing, backspacing and deleting to be within a single undoable session.
Note that Google Docs and Quip treat a switch between typing, backspacing and deleting as disruptions to the continuous typing sessions.

**Start with Selected Text and Followup with Typing**
Let’s start with some selected text. The user may have selected the text directly, as a result of pasting text or through some other sequence of steps. When followed by continuous typing, the initial selection should be included along with the following typing events as part of a single coalesced undo session.

**Timeouts**
Text editing is generally a modeless user experience and so just about any event beyond the above typing actions will disrupt continuous typing. The first and foremost is a timeout.
Quip seems to require continuous typing to occur within 1 second intervals, whereas Google Docs requires about 2 seconds. In my opinion, the timeout may stretch to as long as 3 to 5 seconds. The more time between typing events, the more chance the user has to intentionally input a block of text, a thought or concept.
The use of a timeout is not necessarily required, and the amount of time is certainly up for debate. Collaborative co-editing also makes use of timeouts when grouping text that is merged across clients. Longer timeouts tend to make collaboration a smoother, less disruptive experience, because receiving other client input while expressing one’s thoughts tends to be unhelpful.
Implementers should consider the correct timeout based on their customer expectations and related collaborative features that might share this timeout.

**Hard and Soft Returns**
Hard returns as generated by pressing the Return key tend to disrupt and thereby reset continuous typing.
For Google Docs and Quip, soft returns as generated by pressing shift+Return key do not terminate continuous typing. This makes sense in that the soft return ends text wrap but does not end the paragraph.

**Changing Selections and Key Focus**
Any change to the selection by the user should terminate coalescing. This includes:

- Arrow gesture navigation.
- Gestures to select a word, a sentence or a paragraph.
- Any other selection change that does not move the blinking caret to the next or previous character by way of the above mentioned typing events.

Additionally, ending keyboard focus on the editing window, including moving focus to another editing window or a different view within the same window should also terminate coalescing. This also includes setting focus to modal popovers, such as, for example, a hyperlink property editor.

**Composed Characters**
Character composition occurs when typing diacritics, CJK and even emojis. The process usually involves at least these three phases:

- Start composition.
- Replace composed text with other text and remain composing. This step may occur many times within a single composition session.
- Stop composition either through accepting the text or canceling the latest input.

Coalescing should terminate at start composition. Replacing composed text should generally be coalesced. Stopping composition may or may not reset coalescing depending upon the implementation. Quip appears to try to coalesce multiple composed characters, but disrupts coalescing when switching between composed and non-composed characters. Google Docs appears to disrupt coalescing more frequently. Both coalesce the text replacements.

**Auto Correct, Spelling Corrections and Text Substitutions**
Text substitution behaves much like composed characters. When the text is transformed, the coalescing should terminate and the transformed text becomes the first coalesced entry in the next undo session.

**Copy & Pasting**
Any actions like copy and paste typically trigger events that are not part of the 3 continuous typing actions described above. So, copy and paste also terminates coalescing.

**In Conclusion**
Text undo coalescing greatly improves the usability of undo in that it allows the user to bypass all the intermediate events that went into forming or re-forming text within a paragraph. Given the modeless nature of text editing, limiting continuous typing to strictly three events allows for a robust and clean implementation. Implementers will need tight bottlenecks to trap selection changes, key focus changes as well as changes from collaborative clients. While the above is not complete by any means, I hope readers will find it helpful.

## Tuning HistoryExtension for memory and long sessions

`HistoryExtension` ships with config values chosen for the common case
of a short-lived editor on a richly interactive surface. For
applications that keep an editor mounted for a long time — chat
threads, long documents, anything where the user may produce thousands
of distinct history events without reloading the page — two settings
are worth tuning explicitly.

### `maxDepth` — cap the undo stack

By default `maxDepth` is `null`, meaning the undo stack is
**unbounded**. Every separate-history boundary (see "What Constitutes
Continuous Typing?" above) pushes a `HistoryStateEntry` that retains a
full `EditorState` snapshot. Snapshots share a copy-on-write
`NodeMap`, so the marginal cost of any one entry is small, but it is
not zero — and an unbounded stack accumulates a long tail over time.

Set a finite `maxDepth` to keep the stack at a fixed length. The
extension FIFO-evicts the oldest entries past the cap; older snapshots
become unreachable and can be collected.

```ts
import {configExtension, defineExtension} from 'lexical';
import {HistoryExtension} from '@lexical/history';

const editorExtension = defineExtension({
  // ...
  dependencies: [
    // 100 events is a reasonable starting point that supports a deep
    // enough undo stack for almost any interactive editing session.
    configExtension(HistoryExtension, {maxDepth: 100}),
  ],
});
```

The cap is also exposed as a writable signal on
`HistoryExtension.output.maxDepth`, so it can be reconfigured at
runtime (the new value applies to future pushes — the current stack
is not retroactively trimmed when the cap is lowered).

### `delay` — coalesce more aggressively

`delay` is the merge window during which adjacent edits collapse into
the current history entry instead of pushing a new one. The default is
`300`ms. A longer delay means fewer history events for the same
typing burst, which:

- reduces the rate at which `undoStack` grows, multiplying the effect
  of any `maxDepth` cap, and
- gives the user undo gestures that match larger semantic chunks of
  their writing.

For text-heavy editors a value in the `500`–`1000`ms range is common.

```ts
configExtension(HistoryExtension, {delay: 500, maxDepth: 100}),
```

### When NOT to cap history

If your editor is short-lived (mounted only for the duration of a
modal, page form, or single message draft) and undo is expected to
reach back to the document's initial state, the default `maxDepth:
null` is fine — the stack will be discarded with the editor instance.
The cap matters when an editor is **long-lived** and can accumulate
thousands of distinct history events without unmounting.

---

### Packages Lexical Website Docs Concepts Key Management (`packages/lexical-website/docs/concepts/key-management.md`)

# Key Management

Keys are a fundamental concept in Lexical that enable efficient state management and node tracking. Understanding how keys work is crucial for building reliable editor implementations.

## What are Keys?

The `__key` property is a unique identifier assigned to each node in the Lexical editor. These keys are:
- Automatically generated by Lexical
- Used to track nodes in the editor state
- Essential for state management and updates
- Immutable during a node's lifecycle

## When to Use `__key`?

### ✅ Correct Usage

Keys should ONLY be used in two specific situations:

1. **In Node Constructors**
```typescript
class MyCustomNode extends ElementNode {
  constructor(someData: string, key?: NodeKey) {
    super(key); // Correctly passing key to parent constructor
    this.__someData = someData;
  }
}
```

2. **In Static Clone Methods**
```typescript
class MyCustomNode extends ElementNode {
  static clone(node: MyCustomNode): MyCustomNode {
    return new MyCustomNode(node.__someData, node.__key);
  }
}
```

### ❌ Incorrect Usage

Never use keys in these situations:

```typescript
// ❌ Don't pass keys between different nodes
const newNode = new MyCustomNode(existingNode.__key);

// ❌ Don't manipulate keys directly
node.__key = 'custom-key';
```

## How Lexical Uses Keys

### Diagram

The dotted outlines show nodes that are re-used in a zero-copy fashion from one EditorState to the next

```mermaid
graph TB
  subgraph s0["Initial State"]
    direction TB
    m0["NodeMap (v0)"]
    m0 -->|Key A| n0a["A (v0)"]
    m0 -->|Key B| n0b["B (v0)"]
  end
  subgraph s1["Create Node C"]
    direction TB
    style n1a stroke-dasharray: 5 5
    style n1b stroke-dasharray: 5 5
    m1["NodeMap (v1)"]
    m1 -->|Key A| n1a["A (v0)"]
    m1 -->|Key B| n1b["B (v0)"]
    m1 -->|Key C| n1c["C (v0)"]
  end
  subgraph s2["Update Node A"]
    direction TB
    style n2a stroke-dasharray: 5 0
    style n2b stroke-dasharray: 5 5
    style n2c stroke-dasharray: 5 5
    m2["NodeMap (v1)"]
    m2 -->|Key A| n2a["A (v1)"]
    m2 -->|Key B| n2b["B (v0)"]
    m2 -->|Key C| n2c["C (v0)"]
  end
  s0 -.-> s1 -.-> s2
```

### Node Map Structure

The EditorState maintains a `Map<NodeKey, LexicalNode>` that tracks all nodes. Nodes refer to each other using keys in their internal pointers:

```typescript
// Internal node structure (not for direct usage)
{
  __prev: null | NodeKey,
  __next: null | NodeKey,
  __parent: null | NodeKey,
  // __first, __last and __size are only for ElementNode to track its children
  __first: null | NodeKey,
  __last: null | NodeKey,
  __size: number
}
```

These internal pointers maintain the tree structure and should never be manipulated directly.

### Key-Related APIs

1. **Editor Methods**
   ```typescript
   // Get node by key
   const node = $getNodeByKey(key);

   // Get the DOM element a node key is reconciled to
   const element = editor.getElementByKey(key);

   // Get latest version of a node
   const latest = node.getLatest();
   
   // Get mutable version for updates
   const mutable = node.getWritable();
   ```

### Key Lifecycle

NodeKeys are ephemeral and have several important characteristics:

1. **Serialization**
   - Keys are never serialized
   - New keys are generated when deserializing (from JSON/HTML)
   - Keys are only meaningful within their EditorState instance

2. **Uniqueness**
   - Keys are unique within an EditorState
   - Current implementation uses serial numbers for debugging
   - Should be treated as random and opaque values
   - Never logically reused

Keys are used internally by Lexical to:
1. Track nodes in the editor state
2. Manage node updates and versions
3. Maintain referential integrity
4. Enable efficient state updates

## Common Pitfalls

1. **Key Reuse**
   ```typescript
   // ❌ Never do this
   function duplicateNode(node: LexicalNode) {
     return new SameNodeType(data, node.__key);
   }
   ```

2. **Manual Key Assignment**
   ```typescript
   // ❌ Never do this
   node.__key = generateCustomKey();
   ```

3. **Incorrect Constructor/Clone Implementation**
   ```typescript
   // ❌ Never do this - missing key in constructor
   class MyCustomNode extends ElementNode {
     constructor(someData: string) {
       super(); // Missing key parameter
       this.__someData = someData;
     }
   }

   // ✅ Correct implementation
   class MyCustomNode extends ElementNode {
     __someData: string;

     constructor(someData: string, key?: NodeKey) {
       super(key);
       this.__someData = someData;
     }
     
     static clone(node: MyCustomNode): MyCustomNode {
       return new MyCustomNode(node.__someData, node.__key);
     }

     afterCloneFrom(prevNode: this): void {
       super.afterCloneFrom(prevNode);
       this.__someData = prevNode.__someData;
     }
   }
   ```

4. **Node Replacement**
   ```typescript
   // ❌ Never re-use the key when changing the node class
   const editorConfig = {
     nodes: [
       CustomNodeType,
       {
         replace: OriginalNodeType,
         with: (node: OriginalNodeType) => new CustomNodeType(node.__key),
         withKlass: CustomNodeType
       }
     ]
   };

   // ✅ Correct: Use node replacement configuration
   const editorConfig = {
     nodes: [
       CustomNodeType,
       {
         replace: OriginalNodeType,
         with: (node: OriginalNodeType) => new CustomNodeType(),
         withKlass: CustomNodeType
       }
     ]
   };
   ```

   For proper node replacement, see the [Node Replacement guide](node-replacement.md).

## Best Practices

1. **Let Lexical Handle Keys**
   ```typescript
   import {$applyNodeReplacement} from 'lexical';

   // Create node helper function
   export function $createMyCustomNode(data: string): MyCustomNode {
     return $applyNodeReplacement(new MyCustomNode(data));
   }
   ```

## Testing Considerations

When writing tests involving node keys:

```typescript
test('node creation', async () => {
  await editor.update(() => {
    // ✅ Correct: Create nodes normally
    const node = new MyCustomNode("test");
    
    // ✅ Correct: Keys are automatically handled
    expect(node.__key).toBeDefined();
    expect(node.__key).not.toBe('');
  });
});
```

## Performance Impact

Understanding key management is crucial for performance:

1. Keys enable efficient node lookup (O(1))
2. Proper key usage prevents unnecessary re-renders
3. Lexical's key system optimizes state updates
4. Improper key manipulation can cause performance issues

## Common Questions

**Q: How do I reference a node later?**
A: Store a reference to the node. Conventionally, all node methods will use `getLatest()` or `getWritable()` which will look up the latest version of that node before reading or writing its properties, which is equivalent to using the key but is type-safe (but may cause errors if you try to use a reference to a node that no longer exists). In some situations it may be preferable to use the key directly, which is also fine.

**Q: How do I ensure unique nodes?**
A: Let Lexical handle key generation and management. Focus on node content and structure.

---

### Packages Lexical Website Docs Concepts Keyboard Accessibility (`packages/lexical-website/docs/concepts/keyboard-accessibility.md`)

# Keyboard Accessibility

Lexical follows the [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/) keyboard model. This page documents the contracts that plugin authors and host applications rely on, so a Lexical editor can sit inside a larger keyboard-accessible flow without trapping or surprising the user.

## Tab key

By default, `Tab` inside the editor follows the browser's normal focus order — it moves to the next focusable element on the page. There is no keyboard trap.

The optional `TabIndentationExtension` (`@lexical/extension`) takes over `Tab` and uses it for block indent / outdent. Hosts that mount this extension opt in to that behavior. WCAG 2.1.2 (No Keyboard Trap) still applies — pressing `Escape` blurs the editor (see below), at which point `Tab` leaves the editor again.

## Escape key

Pressing `Escape` while the editor has focus calls `editor.blur()` via the rich-text Escape command handler (registered at `COMMAND_PRIORITY_EDITOR`). The handler only blurs when the current selection is a range selection; with a `NodeSelection` (or no selection) it returns `false` and falls through without blurring. After blur, the next `Tab` advances to the page's next focusable element — so the editor is not a hard keyboard trap. (`TabIndentationExtension` consumes `Tab` only while a range selection is active, and its own source notes that taking over `Tab` is generally discouraged for keyboard accessibility, so treat WCAG 2.1.2 conformance as something to verify in your integration rather than a guarantee.)

Plugin authors who want to intercept `Escape` (modal close, dropdown close, etc.) should register their handler at a higher priority and return `true` to stop the command chain. Common patterns already established in the codebase:

| Priority | Caller | Purpose |
| --- | --- | --- |
| `HIGH` | Floating link editor | Close the floating UI, keep editor focus |
| `HIGH` | Equation component (while editing) | Close the equation editor input |
| `NORMAL` | Comment plugin | Cancel inline comment editing |
| `LOW` | Image component | Escape out of caption / resize editing: drop the inner selection and restore the `NodeSelection` on the image decorator |
| `EDITOR` | Rich text (default) | `editor.blur()` (range selection only) |

Handlers that return `false` let the chain continue, so Escape can still fall through to the default blur even after a higher-priority handler runs.

Some host UI binds its close handler directly to a DOM `keydown` listener rather than going through `KEY_ESCAPE_COMMAND` — the playground's `Modal` via `window.addEventListener('keydown', ...)` and its `DropDown` via a React `onKeyDown` on the menu. Those paths run alongside the table above; they do not participate in the editor's command priority chain.

## React hooks and `@lexical/a11y` extensions

The keyboard helpers below ship as `@lexical/a11y` _extensions_ — the extension is the API and does all of the work (event listeners, focus management, the live region). The `@lexical/react` hooks (`useLexicalFocusManagerRef`, `useLexicalRovingTabIndexRef`, `useLexicalFocusTrapRef`, `useLexicalAriaLiveRegion`) are thin React adapters that only _communicate_ with their extension: each returns a `RefCallback` (or an `announce` function) that drives the extension's signals. Using a hook therefore **requires** its matching extension to be registered in the editor's extension tree — it is not an optional integration path. Non-React hosts (Svelte, Vue, vanilla DOM) use the extensions directly and skip the hooks.

## Toolbar focus jump

The playground uses `useLexicalFocusManagerRef` (`@lexical/react/useLexicalFocusManagerRef`) to provide a documented shortcut for moving focus from the editor to the toolbar without tabbing through the page. The hook returns a `RefCallback` that registers the attached element as a focus-managed toolbar. It listens for `Alt+F10` inside the editor and moves focus into the toolbar — preferring the item that currently carries `tabindex="0"` (the active roving item, when paired with `useLexicalRovingTabIndexRef`), and otherwise the first focusable item. While focus is in the toolbar, pressing `Escape` restores both focus and the editor's prior selection via `editor.focus()`. Multiple toolbars can be registered simultaneously. Requires `FocusManagerExtension` from `@lexical/a11y` in the editor's extension tree.

This matches the WAI-ARIA editor menubar pattern and the convention used by Word and CKEditor. Hosts that don't mount `useLexicalFocusManagerRef` get the default browser flow (Tab in, Tab out).

## Toolbar arrow-key navigation

Toolbars that opt into `useLexicalRovingTabIndexRef` (`@lexical/react/useLexicalRovingTabIndexRef`) collapse to a single tab stop: arrow keys move between items inside the toolbar; `Tab` moves past the toolbar as a whole. Items keep `tabindex="-1"` except the active one (`tabindex="0"`). The hook returns a `RefCallback` — multiple containers can be registered simultaneously. Tracks the WAI-ARIA toolbar pattern. Requires `RovingTabIndexExtension` from `@lexical/a11y` in the editor's extension tree.

## Modal focus trap

`useLexicalFocusTrapRef` (`@lexical/react/useLexicalFocusTrapRef`) returns a `RefCallback` that cycles `Tab` / `Shift+Tab` inside a modal container and restores focus to the previously-focused element on deactivation. While active, the trap also installs a document-level `focusin` listener that pulls any focus landing outside the container back to the first focusable descendant (or the container itself) — so it recovers from the browser routing `Tab` out to its own chrome. Although several containers can be _registered_, only one trap should be _active_ at a time: concurrent traps install competing `focusin` listeners and fight over focus. Escape is **not** intercepted by the hook — modal owners handle the close key themselves, so the editor's Escape contract above stays predictable. Requires `FocusTrapExtension` from `@lexical/a11y` in the editor's extension tree.

## Screen reader announcements

`useLexicalAriaLiveRegion` (`@lexical/react/useLexicalAriaLiveRegion`) returns a stable `announce(message)` function. The hook itself mounts no DOM — like the others above, it is a thin adapter that **requires `AriaLiveRegionExtension` from `@lexical/a11y` in the editor's extension tree**. That extension owns the lifecycle of the visually-hidden region (`role="status"`, `aria-atomic`, `aria-live` defaulting to `polite` — configurable via its `politeness` config): the region is created in the extension's `register` step and disposed on the editor's teardown, not on the hook's unmount. `EditorModeAnnounceExtension` (mode transitions via `registerEditableListener`) and `HistoryAnnounceExtension` (undo / redo via `UNDO_COMMAND` / `REDO_COMMAND`) announce through the same region. Both extensions accept optional message overrides for i18n.

## Component ARIA quick reference

Where Lexical already applies ARIA attributes today (set in each component's `createDOM` / `updateDOM`, declaratively in the surrounding React tree, or applied imperatively via `setAttribute` on a managed or portaled element — as the typeahead menu and the live region do):

| Component | role | aria-\* attributes |
| --- | --- | --- |
| `ToolbarPlugin` (playground) | `toolbar` | `aria-label` |
| `Modal` (playground) | `dialog` | `aria-modal`, `aria-labelledby` |
| `LexicalListItemNode` (check list leaf) | `checkbox` | `aria-checked` |
| `LexicalMenu` typeahead popup | `listbox` | `aria-label` |
| Editor root (while typeahead is open) | — | `aria-controls`, `aria-activedescendant` |
| `EquationNode` (playground) | `math` | `aria-label` |
| `useLexicalAriaLiveRegion` region | `status` | `aria-live`, `aria-atomic` |

Custom nodes follow the same pattern: set the attributes in `createDOM` and re-apply in `updateDOM` when the state they reflect changes. There is no central ARIA contract on `LexicalNode`; treat each subclass's `createDOM` as the source of truth.

## Browser notes

Lexical relies on the standard DOM `Escape` contract — verified on Chrome and Safari (macOS, normal window). Safari in fullscreen intercepts `Escape` at the browser level to exit fullscreen; that is the browser's contract, not Lexical's.

## What this page does not cover

- Touch / mobile screen reader gestures. The hooks above share the same DOM contracts with mobile (TalkBack, VoiceOver iOS) but mobile has not been exercised in this set of changes.
- High contrast / forced-colors theming. See the playground's `index.css` `@media (forced-colors: active)` block for a baseline.

---

### Packages Lexical Website Docs Concepts Listeners (`packages/lexical-website/docs/concepts/listeners.md`)

# Listeners

Listeners are a mechanism that lets the Editor instance inform the user when a certain operation has occurred. All listeners follow a reactive pattern where you can do an operation upon something happening in the future. All listeners also return a function that easily allows for the
listener to be unregistered. Below are the different listeners that Lexical supports today:

## `registerUpdateListener`

Get notified when Lexical commits an update to the DOM.

```js
const removeUpdateListener = editor.registerUpdateListener(({editorState}) => {
  // The latest EditorState can be found as `editorState`.
  // To read the contents of the EditorState, use the following API:

  editorState.read(() => {
    // Just like editor.update(), .read() expects a closure where you can use
    // the $ prefixed helper functions.
  });
});

// Do not forget to unregister the listener when no longer needed!
removeUpdateListener();
```

The update listener callbacks receives a single argument containing the follow properties:

- `editorState` the latest updated Editor State
- `prevEditorState` the previous Editor State
- `tags` a Set of all tags that were passed to the update
- `dirtyElements` a Map of the NodeKeys of ElementNodes that were marked
  dirty, to a boolean that is true when the node was intentionally mutated
- `dirtyLeaves` a Set of the NodeKeys of all dirty nodes that are not
  ElementNodes
- `mutatedNodes` a Map of LexicalNode constructors to a
  `Map<NodeKey, NodeMutation>`, or null when no DOM was mutated. Only
  populated when at least one MutationListener is registered
- `normalizedNodes` (advanced) the keys of TextNode descendants that were
  merged with a sibling by normalization

One thing to be aware of is "waterfall" updates. This is where you might schedule an update inside an update
listener, as shown below:

```js
editor.registerUpdateListener(({editorState}) => {
  // Read the editorState and maybe get some value.
  editorState.read(() => {
    // ...
  });

  // Then schedule another update.
  editor.update(() => {
    // ...
  });
});
```

The problem with this pattern is that it means we end up doing two DOM updates, when we likely could have
done it in a single DOM update. This can have an impact on performance, which is important in a text editor.

A listener that re-enqueues work without a stop condition is worse than slow.
The editor keeps a cascade budget: once a run of updates re-enqueues more
than 100 times without yielding to the event loop, it discards the entire
pending update queue and warns. Bounded activity never reaches the limit —
the budget is reset from a macrotask, which a genuine runaway starves — but a
true loop silently loses whatever else was queued.

To avoid this, we recommend looking into [Node Transforms](transforms.md), which allow you to listen to node changes and
transform them as part of the same given update, meaning no waterfalls!

## `registerTextContentListener`

Get notified when Lexical commits an update to the DOM and the text content of the editor changes from
the previous state of the editor. If the text content is the same between updates, no notifications to
the listeners will happen.

```js
const removeTextContentListener = editor.registerTextContentListener(
  (textContent) => {
    // The latest text content of the editor!
    console.log(textContent);
  },
);

// Do not forget to unregister the listener when no longer needed!
removeTextContentListener();
```

## `registerMutationListener`

Get notified when a specific type of Lexical node has been mutated. There are three states of mutation:

- `created`
- `destroyed`
- `updated`

Mutation listeners are great for tracking the lifecycle of specific types of node. They can be used to
handle external UI state and UI features relating to specific types of node.

If any existing nodes are in the DOM, and skipInitialization is not true, the listener
will be called immediately with an updateTag of 'registerMutationListener' where all
nodes have the 'created' NodeMutation. This can be controlled with the skipInitialization option,
which defaults to false.

```js
const removeMutationListener = editor.registerMutationListener(
  MyCustomNode,
  (mutatedNodes, { updateTags, dirtyLeaves, prevEditorState }) => {
    // mutatedNodes is a Map where each key is the NodeKey, and the value is the state of mutation.
    for (let [nodeKey, mutation] of mutatedNodes) {
      console.log(nodeKey, mutation)
    }
  },
  {skipInitialization: false}
);

// Do not forget to unregister the listener when no longer needed!
removeMutationListener();
```

## `registerEditableListener`

Get notified when the editor's mode has changed. The editor's mode can be changed
via `editor.setEditable(boolean)`.

```js
const removeEditableListener = editor.registerEditableListener(
  (editable) => {
    // The editor's mode is passed in!
    console.log(editable);
  },
);

// Do not forget to unregister the listener when no longer needed!
removeEditableListener();
```

## `registerDecoratorListener`

Get notified when the editor's decorator object changes. The decorator object contains
all `DecoratorNode` keys -> their decorated value. This is primarily used with external
UI frameworks.

```js
const removeDecoratorListener = editor.registerDecoratorListener(
  (decorators) => {
    // The editor's decorators object is passed in!
    console.log(decorators);
  },
);

// Do not forget to unregister the listener when no longer needed!
removeDecoratorListener();
```

## `registerRootListener`

Get notified when the editor's root DOM element (the content editable Lexical attaches to) changes. This is primarily used to
attach event listeners to the root element. *The root listener function is executed directly upon registration and then on any subsequent update.*

```js
const removeRootListener = editor.registerRootListener(
  (rootElement, prevRootElement) => {
   //add listeners to the new root element
   //remove listeners from the old root element
  },
);

// Do not forget to unregister the listener when no longer needed!
removeRootListener();
```

---

### Packages Lexical Website Docs Concepts Named Slots (`packages/lexical-website/docs/concepts/named-slots.md`)

# Named Slots

:::warning

Named slots are experimental. The APIs described here are tagged
`@experimental`, may change without a major version bump, and the
serialization and collab formats should be treated as unstable until the
feature is stabilized.

:::

Named slots let a single host node (an ElementNode or a DecoratorNode) own
several isolated editable regions addressed by name — a Card's `title`, a
PullQuote's `quote` and `attribution` — inside the host's own EditorState.
Each region takes its own caret and formatting, never merges across the
boundary, and `Cmd+A` doesn't spill into the rest of the document.

Before named slots, the usual answer was a nested editor per region: every
region lives in its own EditorState, so moving nodes between regions and
keeping history and collab in sync all require serialization and extra
`editor.update` passes. The other node shapes don't fit either: plain
ElementNode children share one undivided linked list (Backspace at a region
start merges it into the previous region), and a DecoratorNode is atomic, so
Lexical can't own selection, collab, or serialization inside it. With named
slots, editing a slot is just editing the one tree.

:::note

Named slots are the model-level generalization of the rendering concept
behind [`$getDOMSlot`](/docs/api/modules/lexical#getdomslot) /
`ElementDOMSlot` in the
[DOM rendering](../serialization/dom-render.md) documentation. Every
ElementNode already has one _unnamed_ children channel, and `$getDOMSlot`
controls where that channel's content attaches in the node's DOM. Named
slots are additional, explicitly-named channels alongside it — symmetric in
that each renders into a controllable location in the host's DOM — but they
also carry the model-level semantics the unnamed channel doesn't: isolation
(the virtual shadow root below), a separate NodeKey map, and their own
serialization and collaboration.

:::

## The Model

A host keeps a second child channel, a `Map` of slot name to NodeKey,
separate from its ordinary linked-list children. A slot value has its slot
host pointer set and `getParent() === null`, with exactly one of the two
non-null: `getParent()` stops at the slot boundary, and you climb out only
through [`$getSlotHost()`](/docs/api/modules/lexical#getslothost).

The slot link itself acts as a **virtual invisible shadow root** between the
host and the value. Isolation is structural rather than a convention — an
accidental boundary crossing surfaces as a thrown invariant, not silent
corruption.

In the DOM, each value renders synchronously into a keyless
`<div data-lexical-slot="<name>">` container parked slots-first in the host
DOM as a **hidden placeholder** (`display: none`), in a canonical order
derived from the host class (see [slot order](#slot-order)). Nothing is
visible until the host explicitly attaches the container somewhere (see
[Rendering](#rendering)) — mirroring how
[`getDOMSlot`](/docs/api/modules/lexical#getdomslot) gives an element
control over where its linked-list children render.

## Hosts and Values

Any ElementNode or DecoratorNode can host slots. An ElementNode host can mix
slots with ordinary children (a Card with a `title` slot plus regular body
paragraphs); a DecoratorNode host has no children channel, so slots are its
only editable content (a PullQuote that is otherwise atomic).

A slot value is any non-inline ElementNode or DecoratorNode. The value's
shape decides the editing model:

- **A bare block** (for example a ParagraphNode) behaves as a single-line
  field with no extra wrapper in the model, the JSON, or the DOM: its virtual
  scope holds exactly one block, so Enter is a no-op (hosts may map it to
  focus movement) and a multi-block paste flattens to inline content the way
  an `<input>` sanitizes its value — line breaks stripped, block-only
  decorators dropped.
- **A shadow-root container** (an ElementNode whose
  [`isShadowRoot()`](/docs/api/modules/lexical#isshadowroot) returns
  `true`) behaves as a multi-block region with normal block editing inside.

## Declaring and Setting Slots

A host class declares its slot names in [`$config()`](nodes.mdx); values are
attached with [`$setSlot`](/docs/api/modules/lexical#setslot):

```ts
import {
  $create,
  $createParagraphNode,
  $setSlot,
  ElementNode,
} from 'lexical';

class CardNode extends ElementNode {
  $config() {
    return this.config('card', {extends: ElementNode, slots: ['title']});
  }
  createDOM(): HTMLElement {
    return document.createElement('div');
  }
  updateDOM(): boolean {
    return false;
  }
}

function $createCardNode(): CardNode {
  const card = $create(CardNode);
  // Single-line title: the bare paragraph IS the slot value. An empty
  // paragraph is an empty field; to seed default text, append a non-empty
  // TextNode (empty TextNodes are eliminated during reconciliation).
  $setSlot(card, 'title', $createParagraphNode());
  // Ordinary body child, edited like any other block.
  return card.append($createParagraphNode());
}
```

For a multi-block region, use a shadow-root container as the value:

```ts
class SlotContainerNode extends ElementNode {
  $config() {
    return this.config('slot-container', {extends: ElementNode});
  }
  createDOM(): HTMLElement {
    return document.createElement('div');
  }
  updateDOM(): boolean {
    return false;
  }
  isShadowRoot(): boolean {
    return true;
  }
}

$setSlot(
  pullQuote,
  'quote',
  $create(SlotContainerNode).append(
    $createParagraphNode().append($createTextNode('First block')),
    $createParagraphNode().append($createTextNode('Second block')),
  ),
);
```

The core API surface (all exported from `lexical`):

- [`$setSlot(host, name, node)`](/docs/api/modules/lexical#setslot) — place a value into a named slot, replacing
  any existing value under that name. Move semantics, mirroring `append`:
  the value is detached from any current parent or slot first. Throws if the
  value is inline or if the name is one of the reserved prototype keys
  (`__proto__`, `constructor`, `prototype`). Slotting a node into its own
  descendant would form a cycle; this is checked (and throws) in development
  only — in production it behaves like appending an ancestor through the
  children channel, which is likewise unguarded.
- [`$getSlot(host, name)`](/docs/api/modules/lexical#getslot) — the value under a name, or `null`.
- [`$getSlotNames(host)`](/docs/api/modules/lexical#getslotnames) — the host's slot names in canonical order.
- [`$removeSlot(host, name)`](/docs/api/modules/lexical#removeslot) — detach the value under a name (the subtree is
  garbage-collected unless you reattach it elsewhere).
- [`$getSlotHost(node)`](/docs/api/modules/lexical#getslothost) — the host a value is slotted into, or `null`.
- [`$getSlotNameWithinHost(node)`](/docs/api/modules/lexical#getslotnamewithinhost) — the name of the slot
  a value occupies on its host (the reverse direction of `$getSlotHost`), or
  `null` when the node is not a slot value.
- [`$getSlotFrame(node)`](/docs/api/modules/lexical#getslotframe) — the innermost slot value containing a node (the
  "frame" whose virtual shadow root scopes editing), or `null` when the node
  is not inside any slot.
- [`$getSelectionSlotFrame(selection)`](/docs/api/modules/lexical#getselectionslotframe) — the slot frame a
  selection lives in, or `null` when it is outside any slot. Selection-driven
  exporters walk this frame instead of the root's children, because a
  selection inside a slot never contains its host and a root walk would
  export nothing. Works for every selection type, not just `RangeSelection`.
- [`$isSlotHost(node)`](/docs/api/modules/lexical#isslothost) /
  [`$isSlotChild(node)`](/docs/api/modules/lexical#isslotchild) — type guards
  for the [`SlotHostNode`](/docs/api/modules/lexical#slothostnode) /
  [`SlotChildNode`](/docs/api/modules/lexical#slotchildnode) interfaces.

## Slot Order

Slot order is canonical and derived, never stored. Names declared in
`$config()` (`slots: ['quote', 'attribution']`) sort first in declaration
order; undeclared names sort after them, lexicographically by UTF-16 code
unit (plain JavaScript string comparison, locale-independent). `$setSlot`
re-canonicalizes on every write, so documents normalize on load and
concurrent collaborative additions converge to the same order on every
client. If presentation order matters, declare the names.

## Rendering

The reconciler always renders every slot subtree synchronously, but into a
hidden placeholder container — visibility is the host's explicit decision.
There are three ways to attach a slot, all sharing the same contract
(attaching moves the container to the target, a no-op when it is already
there, and reveals it; the container renders as a normal block):

1. **Synchronously in-lexical**: register a `$getSlotTargetElement`
   [`DOMRenderMatch`](/docs/api/modules/lexical_html#domrendermatch)
   override for the host's node class (a
   [DOM render override](../serialization/dom-render.md), not a node
   method — this is an advanced hook). The reconciler consults it whenever
   it creates or reconciles the slot's container and attaches/reveals
   within the same commit — no listener or framework hop. Returning
   `hostDom` reveals the slot in its default slots-first position:

   ```ts
   import {domOverride, DOMRenderExtension} from '@lexical/html';
   import {configExtension, defineExtension} from 'lexical';

   export const CardExtension = defineExtension({
     dependencies: [
       configExtension(DOMRenderExtension, {
         overrides: [
           domOverride([CardNode], {
             // Reveal the title in its default slots-first position within
             // the same commit that renders it. Returning an element from
             // deeper inside the host's DOM attaches it there instead;
             // $next() defers to lower-priority overrides (default: null,
             // a hidden placeholder).
             $getSlotTargetElement: (node, slotName, hostDom, $next, editor) =>
               hostDom,
           }),
         ],
       }),
     ],
     name: 'card',
     nodes: [CardNode],
   });
   ```

2. **Imperatively**:
   [`mountSlotContainer(editor, nodeKey, slotName, target)`](/docs/api/modules/lexical#mountslotcontainer)
   and
   [`unmountSlotContainer(editor, nodeKey, container)`](/docs/api/modules/lexical#unmountslotcontainer)
   from `lexical` are the framework-independent primitives (e.g. from a
   [mutation listener](/docs/api/modules/lexical#registermutationlistener),
   which fires after the commit). `mountSlotContainer` resolves the container
   against the committed editor state
   ([`editor.getEditorState()`](/docs/api/modules/lexical#geteditorstate)), so
   the model it reads matches the reconciled DOM it reveals;
   `unmountSlotContainer` takes the container you already hold and only touches
   the DOM:

   ```ts
   import {mountSlotContainer} from 'lexical';

   // e.g. inside an extension's register(editor):
   const unregister = editor.registerMutationListener(
     CardNode,
     (mutations) => {
       for (const [nodeKey, mutation] of mutations) {
         if (mutation === 'destroyed') {
           continue;
         }
         const hostDom = editor.getElementByKey(nodeKey);
         if (hostDom !== null) {
           // Mounting in place: the placeholder is already parked in the
           // host DOM, so this just reveals it in its slots-first position.
           // Any element within the host's DOM works as a target.
           mountSlotContainer(editor, nodeKey, 'title', hostDom);
         }
       }
     },
     {skipInitialization: false},
   );
   ```

   `unmountSlotContainer(editor, nodeKey, container)` is the reverse: it
   hides the container and parks it back in the host DOM, for when a mount
   target goes away while the host remains.

3. **From React chrome**: the
   [`useLexicalSlotRef`](/docs/api/modules/lexical_react_useLexicalSlotRef#uselexicalslotref)
   hook from `@lexical/react/useLexicalSlotRef` wraps the imperative pair and returns a
   ref that mounts a slot's container into your component — the usual choice
   for a DecoratorNode host's `decorate()` chrome (whose containers are
   opted back into `contentEditable` automatically, since the decorator DOM
   is non-editable):

```tsx
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {useLexicalSlotRef} from '@lexical/react/useLexicalSlotRef';

function PullQuoteComponent({nodeKey}: {nodeKey: NodeKey}) {
  const [editor] = useLexicalComposerContext();
  const quoteRef = useLexicalSlotRef<HTMLDivElement>(editor, nodeKey, 'quote');
  const attributionRef = useLexicalSlotRef<HTMLDivElement>(
    editor,
    nodeKey,
    'attribution',
  );
  return (
    <blockquote>
      <div ref={quoteRef} />
      <div ref={attributionRef} />
    </blockquote>
  );
}
```

A `contentEditable=false` ElementNode shell can host React chrome the same
way (slot containers opt into editing whenever the host DOM is
non-editable): the playground's Review demo portals chrome into the host DOM,
attaches the author slot with `useLexicalSlotRef`, drives an interactive
star-rating widget that persists to NodeState, and applies the identical
hidden-then-attach technique to its `getDOMSlot` children element. Such a shell should call
[`setDOMUnmanaged(dom)`](/docs/api/modules/lexical#setdomunmanaged) in
`createDOM` — the portal and the
attach moves mutate the shell's children from outside the reconciler, and
the marker gives the shell the same mutation-observer exemption a
DecoratorNode's DOM has.

## Editable state

A slot rendered inside a non-editable host (a DecoratorNode, or a
`contentEditable=false` element shell) would not track the editor's editable
state on its own, so the reconciler gives its container an explicit
`contentEditable` that follows `editor.isEditable()`, and re-renders the islands
whenever `setEditable` toggles — so a read-only editor's slots are not left
editable. No extension is required. Slots always follow the editor; there is
currently no way for a slot to override its own editable state.

A host that attaches its own editable island which is *not* a slot container —
for example the Review demo's `getDOMSlot` children element inside its
`contentEditable=false` shell — opts it into the same behavior with
[`$markSlotEditable(element, editor)`](/docs/api/modules/lexical#marksloteditable),
re-applying it from `updateDOM` so an editable toggle reaches the island.

## Editing Behavior

- **Selection never crosses a slot boundary.** Selections are clamped to the
  anchor's slot frame at every entry point (DOM resolution,
  [`$setSelection`](/docs/api/modules/lexical#setselection),
  and point mutation), so a mouse drag and a `shift+arrow` across the
  boundary land on the same clamped result. A focus in a slot of a host is
  moved past the whole host, or to the edge of the host's own children when
  the selection starts inside the host.
- **Deletion stops at the boundary.** Backspace at the start of a slot and
  forward-Delete at its end are no-ops instead of merging across the virtual
  shadow root.
- **`Cmd+A` scopes to the slot frame** when the caret is inside one; outside
  slots the default handlers keep the legacy whole-document behavior.
  Progressive expansion (block → enclosing slot frame → document on repeated
  presses) is provided by the opt-in
  [`SelectBlockExtension`](/docs/api/modules/lexical_extension#selectblockextension)
  from `@lexical/extension`.
- **Generic block conversion skips slot values.** A slot value has no
  parent — its up-link is `$getSlotHost` — so selection-driven block
  converters (`$setBlocksType`, `$wrapNodes`, `$insertList` / `$removeList`,
  the markdown block shortcuts) treat it as ineligible and do nothing rather
  than replace it: the slot assignment is managed by the node or extension
  that owns the slot. Calling `LexicalNode.replace` directly on a slot value
  throws; reassign the slot with `$setSlot` on its host instead.
- **Slots stay bound to their host through `replace`.** Replacing a slot
  *host* (`host.replace(other)`) does not transfer its slots to the
  replacement — slots are not necessarily portable across node types, so the
  slot map travels with the node, never with its position. If the replaced
  host is reattached in the same update (the
  [`$wrapNodeInElement`](/docs/api/modules/lexical_utils#wrapnodeinelement)
  pattern) it keeps its slots; if it stays detached, its slot subtrees are
  garbage-collected with it. The same applies to anything built on `replace`:
  converting a slot host with `$setBlocksType` produces a slot-less
  replacement block. Move a slot onto another host explicitly with
  `$setSlot`.
- **A NodeSelection of an element carries its children.** Copy and export
  of a whole-host NodeSelection (e.g. a chrome click that selects "the whole
  Card") include the host's body children even though they aren't in the
  selection themselves — the old shell-only output made cut silently lossy.
  This applies only to NodeSelection; a partial RangeSelection over the host
  keeps per-child slicing.

### Traversal is intentionally asymmetric

Content reads include slot subtrees, slots-first:
[`getTextContent()`](/docs/api/modules/lexical#gettextcontent),
[`getAllTextNodes()`](/docs/api/modules/lexical#getalltextnodes),
and the [`$dfsWithSlots`](/docs/api/modules/lexical_utils#dfswithslots)
family in `@lexical/utils` count slot content for search, copy, and
accessibility. Navigation excludes them:
[`getChildren()`](/docs/api/modules/lexical#getchildren),
[`getFirstDescendant()`](/docs/api/modules/lexical#getfirstdescendant)
and friends stay linked-list-only, so caret movement never walks into a slot
by accident. Choose [`$dfs`](/docs/api/modules/lexical_utils#dfs) or
[`$dfsWithSlots`](/docs/api/modules/lexical_utils#dfswithslots) depending on whether "this subtree" should mean the
navigable tree or all content.

## Serialization

JSON serialization is automatic in both directions. A host's slots serialize
under the reserved `$slots` key on
[`SerializedLexicalNode`](/docs/api/modules/lexical#serializedlexicalnode)
(a sibling of
NodeState's reserved `'$'` key), keyed by slot name:

```json
{
  "type": "card",
  "version": 1,
  "$slots": {
    "title": {"type": "paragraph", "children": [], "version": 1}
  },
  "children": []
}
```

Parsing re-attaches each subtree with `$setSlot` and throws if `$slots`
appears on a node that cannot host slots. The `$` prefix keeps the
framework-owned key from colliding with a subclass that already serializes a
`slots` property of its own.

HTML serialization is opt-in per host, like NodeState: the exporter never
descends into slots on its own. A host's `exportDOM` can emit each slot into
a wrapper using
[`$appendNodeToHTML`](/docs/api/modules/lexical_html#appendnodetohtml) from
`@lexical/html`, and a
[DOM import rule](../serialization/dom-import.md) on the host's
distinguishing markup maps the wrappers back through `$setSlot`.

## Collaboration

Slots sync on both the V1 and V2 Yjs bindings through a per-slot-diffed
`Y.Map` stored under the reserved `__slots` attribute key on the host's shared
type (the channel reuses the host's `__slots` field name, which is already
excluded from the property sync). Hosts with declared slots create that map
eagerly, so two clients
concurrently setting *different* slot names for the first time merge
per-entry instead of racing. Malformed or hostile remote entries are
validated and skipped.

:::caution

Pre-slots clients receiving slot data from upgraded peers will error rather
than render it. Enabling slots in long-lived shared documents should be
gated on all participants running a slots-aware version; new clients
tolerate unknown slot data going forward.

:::

## Reserved Names

Adding slots reserves a few identifiers that custom node subclasses should
not define for their own purposes:

- the `__slots` and `__slotHost` fields on ElementNode / DecoratorNode — the
  `__slots` name is also the collab attribute key for the slots channel, so it
  is reserved on the Yjs shared type too;
- the `$slots` serialized JSON key.

## Current Limitations

- The caret / NodeCaret APIs throw across a slot boundary ("no common
  ancestor"); slot-aware caret traversal is a planned follow-up.
- Nested slots (a slot whose host is itself slotted) are only handled one
  level deep by the runtime selection comparator.
- The mixed-version collab caveat above.

---

### Packages Lexical Website Docs Concepts Node Cloning (`packages/lexical-website/docs/concepts/node-cloning.md`)

# Node Cloning

Understanding how to properly clone and modify nodes is crucial for working with Lexical. This guide explains the different cloning mechanisms and when to use each one.

## Types of Node Cloning

Lexical provides several ways to clone nodes, each serving a different purpose:

1. `static clone()` - Internal API for state management
2. `$copyNode()` - Public API for creating new nodes
3. `getWritable()` - Public API for node modifications

## The `clone` Method

### What is `clone`?

The `clone` method is a static method that creates the next version of a node. A node that declares [`$config()`](./nodes.mdx) has one synthesized for it and need not implement it — a hand-written `clone` is only required for a node that does not use `$config()`, or whose constructor has required arguments. Either way, it's important to understand that this is an **internal API** used by Lexical's state management system.

```typescript
class MyCustomNode extends ElementNode {
  static clone(node: MyCustomNode): MyCustomNode {
    // ✅ Correct implementation
    return new MyCustomNode(node.__someData, node.__key);
  }
}
```

### When is `clone` Used?

1. **Internal State Management**
   - Used by Lexical internally
   - Part of the editor's state update system
   - Called by `getWritable()`

2. **Node Mutations**
   ```typescript
   // Example of a node method implementation
   class MyCustomNode extends ElementNode {
     setData(data: string): this {
       const self = this.getWritable();
       self.__data = data;
       return self;
     }
   }

   node.setData("new data");
   ```

### When NOT to Use `clone`?

```typescript
// ❌ Never do this
function $duplicateNode(node: MyCustomNode) {
  return MyCustomNode.clone(node); // Don't call clone directly
}
```

## The `afterCloneFrom` Method

### What is `afterCloneFrom`?

`afterCloneFrom(prevNode)` runs on every clone, right after it is constructed,
and copies over whatever the constructor did not. The base implementation
carries the node's links to its parent and siblings and its
[NodeState](./node-state.md), and `ElementNode` and `TextNode` carry their own
built-in properties such as format, style, and indent. A property that is
copied neither by the constructor nor by some `afterCloneFrom` silently
reverts to its default on the node's next write.

### When you don't need it

With the modern APIs you usually don't write one at all:

- **Serialization schema.** A property declared as a field in a `$config`
  [`json` schema](./nodes.mdx#creating-custom-nodes-with-a-serialization-schema)
  (`withField(stringValue(), {field: '__label'})`) is carried across the
  clone for you, because the schema says where it is stored. Available in
  Lexical v0.51.0 and later.
- **NodeState.** State created with `createState` and stored with `$setState`
  is carried by the base `afterCloneFrom`, so a node whose extra data lives
  only in NodeState needs nothing.

```ts
// At module scope, above the class, so TypeScript checks it against the class
const calloutSchema = nodeSchema<CalloutNode>()({
  label: withField(stringValue(), {field: '__label'}),
});

class CalloutNode extends ElementNode {
  __label: string = '';

  // No afterCloneFrom: `__label` is declared as a field in calloutSchema, so
  // it is carried across every clone.
  $config() {
    return this.config('callout', {extends: ElementNode, json: calloutSchema});
  }
}
```

### When you still need it

Override `afterCloneFrom` for a property that neither of those covers: a
field that isn't in the node's schema, one declared only through accessor
methods (see
[Carrying properties across a clone](../serialization/serialization.md#carrying-properties-across-a-clone)),
or any property of a node written without a schema. Always call
`super.afterCloneFrom(prevNode)` first so the inherited properties and
NodeState are still carried:

```ts
class MyCustomNode extends ElementNode {
  __data: string = '';

  afterCloneFrom(prevNode: this): void {
    super.afterCloneFrom(prevNode);
    this.__data = prevNode.__data;
  }
}
```

Like `clone`, `afterCloneFrom` is called by Lexical; don't call it yourself
except through `super`.

## Using `$copyNode`

### What is `$copyNode`?

`$copyNode` is the public API for creating a copy of a node with a new key. Use this when you need to create a duplicate node.
By default, all properties and `NodeState` will be copied to the new node, and then `resetOnCopyNodeFrom` will be called to
allow the node to optionally reset certain properties (and NodeState configured with `resetOnCopyNode: true`) to defaults
(such as the checked state of a `ListItemNode`).

```typescript
// ✅ Correct: Using $copyNode
const copy = $copyNode(existingNode);
```

### When to Use `$copyNode`?

1. **Creating Duplicates**
   ```typescript
   // ✅ Correct: Duplicating a node
   const duplicate = $copyNode(originalNode);
   someParent.append(duplicate);
   ```

## Using `getWritable`

### What is `getWritable`?

`getWritable` is an internal API used within node method implementations to get a mutable version of a node. Node consumers should use the node's public methods instead.

```typescript
// ✅ Correct: Implementation of a node method
class MyCustomNode extends ElementNode {
  setData(data: string): this {
    const self = this.getWritable();
    self.__data = data;
    return self;
  }
}

// ✅ Correct: Using the node
const node = new MyCustomNode();
node.setData("new value");
```

## Common Patterns

### Modifying Nodes

```typescript
// ✅ Correct: Modifying a node
function $updateNodeData(node: MyCustomNode, newData: string): MyCustomNode {
  return node.setData(newData);
}

// ❌ Incorrect: Don't clone manually
function $updateNodeDataWrong(node: MyCustomNode, newData: string): MyCustomNode {
  const clone = MyCustomNode.clone(node);
  clone.setData(newData);
  return clone;
}
```

### Copying Nodes

```typescript
// ✅ Correct: Copying a node
function $duplicateNode(node: MyCustomNode): MyCustomNode {
  return $copyNode(node);
}

// ❌ Incorrect: Don't use clone
function $duplicateNodeWrong(node: MyCustomNode): MyCustomNode {
  return node.constructor.clone(node);
}
```

## Performance Considerations

1. **Referential Integrity**
   - `getWritable()` ensures proper EditorState updates with new clones
   - Prevents "orphaned" nodes that won't be rendered

2. **State Management**
   ```typescript
   class MyCustomNode extends ElementNode {
     setData(data: string): this {
       const self = this.getWritable();
       self.__data = data; // Properly tracked by editor
       return self;
     }
   }
   ```

## Testing

```typescript
test('node modification', async () => {
  await editor.update(() => {
    const node = new MyCustomNode("test");
    
    node.setData("new data");
    
    // ✅ Correct: Use $copyNode for duplication
    const copy = $copyNode(node);
  });
});
```

## Related Concepts

- [Editor State](editor-state.md) - How cloning affects editor state
- [Nodes](nodes.mdx) - Core concepts about Lexical nodes

## Common Questions

**Q: How do I duplicate a node?**
A: Use `$copyNode(node)` to create a new copy with a new key.

**Q: When should I use clone?**
A: Never directly. Use `$copyNode()` or `getWritable()` instead.

---

### Packages Lexical Website Docs Concepts Node Replacement (`packages/lexical-website/docs/concepts/node-replacement.md`)

# Node Replacement

Node Replacement allow you to replace all instances of a given node in your editor with instances of a subclass.

## Use Case

:::note

In earlier versions of this documentation, "Node Replacement" was called "Node Overrides".
We've changed the name to match the terms used in the implementation.

:::

:::tip

If your use case only requires adding ad-hoc data to existing nodes, you may be able to use the [NodeState](node-state.md) API instead of subclassing and node replacement.

:::

Some of the most commonly used Lexical Nodes are owned and maintained by the core library. For example, ParagraphNode, HeadingNode, QuoteNode, List(Item)Node etc - these are all provided by Lexical packages, which provides an easier out-of-the-box experience for some editor features, but makes it difficult to override their behavior. For instance, if you wanted to change the behavior of ListNode, you would typically extend the class and override the methods. However, how would you tell Lexical to use *your* ListNode subclass in `ListExtension` instead of using the core ListNode? That's where Node Replacement can help.


Node Replacement allow you to replace all instances of a given node in your editor with instances of a different node class. This can be done through the `nodes` of an extension:

```ts
const CustomParagraphExtension = defineExtension({
  name: '@my-app/CustomParagraph',
  nodes: () => [
    // Don't forget to register your custom node separately!
    CustomParagraphNode,
    {
      replace: ParagraphNode,
      with: (node: ParagraphNode) => {
        return $createCustomParagraphNode();
      },
      withKlass: CustomParagraphNode,
    },
  ],
});
```
In the snippet above,
- `replace`: Specifies the core node type to be replaced. 
- `with`: Defines a transformation function to replace instances of the original node to the custom node.  
- `withKlass`: This option ensures that behaviors associated with the original node type work seamlessly with the replacement. For instance, node transforms or mutation listeners targeting ParagraphNode will also apply to CustomParagraphNode when withKlass is specified. Without this option, the custom node might not fully integrate with the editor's built-in features, leading to unexpected behavior.

Once this is done, Lexical will replace all ParagraphNode instances with CustomParagraphNode instances. One important use case for this feature is overriding the serialization behavior of core nodes. Check out the full example below.

## Node Replacement Example

This example demonstrates using Node Replacement to replace all `ParagraphNode` with a `CustomParagraphNode` that overrides `createDOM`.

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-replacement?file=src/main.tsx)

<iframe width="100%" height="600" src="https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-replacement?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview" sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts" title="Node Replacement Example"></iframe>

---

### Packages Lexical Website Docs Concepts Node State (`packages/lexical-website/docs/concepts/node-state.md`)

# NodeState

The NodeState API introduced in v0.26.0 allows arbitrary state to be added
ad-hoc to any node in a way that participates with reconciliation, history,
and JSON serialization.

## Use Case

NodeState allows your application to define keys that can be stored on
any node with automatic JSON support.

:::tip

You can even add node state to the RootNode to store document-level metadata.

:::

With a combination of NodeState and other APIs such as
[Listeners](listeners.md) or
[Transforms](transforms.md) you can
likely shape the editor to meet your needs without having to do much
[Node Customization](node-replacement.md).

Even when you are subclassing nodes, using NodeState instead of additional
properties to store the node's data can be [more efficient](#efficiency)
and will save you from writing a lot of boilerplate in the constructor,
updateFromJSON, and exportJSON.

## Usage

### createState

[createState](/docs/api/modules/lexical#createstate)
creates a
[StateConfig](/docs/api/modules/lexical#stateconfig)
which defines the key and configuration for your NodeState value.

The key must be locally unique, two distinct StateConfig must not have the
same string key if they are to be used on the same node.

Typical usage will look something like this:

```ts
const questionState = createState('question', {
  parse: (v) => (typeof v === 'string' ? v : ''),
});
```

The required `parse` function serves two purposes:
- It provides a type-safe and runtime-safe way to parse values that were
  serialized to JSON
- When called with `undefined` (or any invalid value) it should return some
  default value (which may be `undefined` or `null` or any other value you
  choose)

In this case, the question must be a string, and the default is an
empty string.

See the
[createState](/docs/api/modules/lexical#createstate)
API documentation for more details, there are other optional settings
that you may want to define particularly if the value is not a primitive
value such as boolean, string, number, null, or undefined.

:::tip

Instead of writing parse functions by hand, use the
[serialization schema combinators](/docs/serialization/#declarative-serialization-schemas-with-config)
from `lexical`, such as `stringValue`, `numberValue`, `enumValue`,
`arrayValue`, `objectValue`, and `nullable`. They are the same ones a node's
`$config` JSON schema uses. A schema is itself a parse function, so it can be
passed directly as `parse`:

```ts
import {arrayValue, createState, stringValue} from 'lexical';

const questionState = createState('question', {parse: stringValue()});
const tagsState = createState('tags', {parse: arrayValue(stringValue())});
```

Because `parse` is just a function, the combinators also compose with schema
libraries such as [zod](https://zod.dev/), [ArkType](https://arktype.io/),
[Effect](https://effect.website/), and [Valibot](https://valibot.dev/). Wrap a
library's validator in a parse function that returns a default for invalid
input, or use `transformValue` to refine what a combinator parsed.

:::

### $getState

[$getState](/docs/api/modules/lexical#getstate) gets the
NodeState value from the given node, or the default if that key was never
set on the node.

```ts
const question = $getState(pollNode, questionState);
```

See also
[$getStateChange](/docs/api/modules/lexical#getstatechange)
if you need an efficient way to determine if the state has changed on two
versions of the same node (typically used in updateDOM, but may be useful in
an update listener or mutation listener).

### $setState

[$setState](/docs/api/modules/lexical#setstate) sets the
NodeState value on the given node.

```ts
const question = $setState(
  pollNode,
  questionState,
  'Are you planning to use NodeState?',
);
```

:::tip

The last argument is a ValueOrUpdater, just like with React's useState
setters. If you use an updater function and the value does not change,
the node and its NodeState *won't* be marked dirty.

:::

## Serialization

The NodeState for a node, if any values are set to non-default values, is
serialized to a record under a single
[NODE_STATE_KEY](/docs/api/modules/lexical#node_state_key)
which is equal to `'$'`.

```json
{
  "type": "poll",
  "$": {
    "question": "Are you planning to use NodeState?"
  }
}
```

:::tip

By default, it is assumed that your parsed values are JSON serializable,
but for advanced use cases you may use values such as Date, Map, or Set
that need to be transformed before JSON serialization. See the
[StateValueConfig](/docs/api/modules/lexical#statevalueconfig)
API documentation.

:::

### Flat serialization with `$config`

Nodes that declare a `StateConfig` in [`$config`](nodes.mdx#creating-custom-nodes-with-config-and-nodestate)
with `flat: true` lift that key to the top level of the serialized node
instead of nesting it under `'$'`. This makes the JSON shape identical
to a legacy node that stored the value as a `__property` instance
variable, so existing payloads continue to round-trip after the node is
migrated to NodeState.

For example, a `ColoredNode` that extends `TextNode` and declares a flat
`color` state (see [Extending TextNode with `$config`](nodes.mdx#extending-textnode-with-config)):

```ts
$config() {
  return this.config('colored', {
    extends: TextNode,
    stateConfigs: [{flat: true, stateConfig: colorState}],
  });
}
```

serializes as:

```json
{
  "type": "colored",
  "text": "hello",
  "color": "red"
}
```

A non-flat (default) state on the same node would instead appear under
`'$'`:

```json
{
  "type": "colored",
  "text": "hello",
  "$": {
    "color": "red"
  }
}
```

In both cases, a key is only emitted when the current value is not equal
to the default returned by its `parse` function — see
[Efficiency](#efficiency).

:::note

Don't reuse a flat key that a superclass already serializes (e.g. `text`
on `TextNode`).

:::

### Upgrading a legacy JSON property to NodeState

A node that stores data as a `__property` instance variable with custom
`exportJSON`/`importJSON`/`updateFromJSON` can be migrated to NodeState
with `flat: true` without changing its serialized JSON shape.

Before — `ColoredNode` with a `__color` property and hand-written
serialization (see the full legacy example in
[Extending TextNode with `$config`](nodes.mdx#extending-textnode-with-config)):

```ts
export type SerializedColoredNode = Spread<
  {color?: string},
  SerializedTextNode
>;

export class ColoredNode extends TextNode {
  __color: string;

  constructor(text: string = '', color: string = DEFAULT_COLOR, key?: NodeKey) {
    super(text, key);
    this.__color = color;
  }

  static getType(): string {
    return 'colored';
  }

  static clone(node: ColoredNode): ColoredNode {
    return new ColoredNode(node.__text, node.__color, node.__key);
  }

  static importJSON(serializedNode: SerializedColoredNode) {
    return new ColoredNode().updateFromJSON(serializedNode);
  }

  updateFromJSON(serializedNode: SerializedColoredNode) {
    const self = super.updateFromJSON(serializedNode);
    self.__color =
      typeof serializedNode.color === 'string'
        ? serializedNode.color
        : DEFAULT_COLOR;
    return self;
  }

  exportJSON(): SerializedColoredNode {
    return {
      ...super.exportJSON(),
      color: this.__color === DEFAULT_COLOR ? undefined : this.__color,
    };
  }
}
```

After — same on-the-wire JSON, no hand-written serialization:

```ts
const colorState = createState('color', {
  parse: (v) => (typeof v === 'string' ? v : DEFAULT_COLOR),
});

export class ColoredNode extends TextNode {
  $config() {
    return this.config('colored', {
      extends: TextNode,
      stateConfigs: [{flat: true, stateConfig: colorState}],
    });
  }
}
```

No `exportJSON`, `importJSON`, `updateFromJSON`, `clone`, or
`afterCloneFrom` override is needed: `$config` installs `clone` and
`importJSON`, and the base `exportJSON`/`updateFromJSON`/`afterCloneFrom`
on `LexicalNode`/`TextNode` already round-trip NodeState. Read the color
via `$getState(node, colorState)` and write it via
`$setState(node, colorState, value)` instead of `node.__color`.

:::tip

For a seamless migration, keep the JSON key name the same — for a flat
state, that's the first argument to `createState`. Migrating from a
non-flat NodeState to a flat NodeState also works: state that appears
under the `$` (`NODE_STATE_KEY`) is still parsed even when the state is
configured as flat, and the flat value takes precedence when both are
present.

:::

## Efficiency

NodeState uses a copy-on-write scheme to manage each node's state. If
none of the state has changed, then the NodeState instance will be
shared across multiple versions of that node.

:::info

In a given reconciliation cycle, the first time a Lexical node is marked dirty
via `getWritable` will create a new instance of that node. All properties
of the previous version are set on the new instance. NodeState is stored
as a single property, and no copying of the internal state is done
until the NodeState itself is marked writable.

:::

When serializing to JSON, each key will only be stored if the value
is not equal to the default value. This can save quite a lot of space
and bandwidth.

Parsing and serialization is only done at network boundaries, when
integrating with JSON or Yjs. When a value changes from an external
source, it is only parsed once the first time it is read.
Values that do not come from external sources are not parsed, and
values that are not used are never parsed.

## Capabilities

Current:

- Allows you to define and add state to any node
- Serializes that state automatically in the node's JSON, supporting
  versioning and copy+paste
- Works with the reconciler, TextNodes with differing state will not
  be implicitly merged
- @lexical/yjs support, NodeState will be automatically synchronized
  like any other property
- NodeState values that are not used will simply pass-through, making
  it a bit easier for situations where multiple configurations are used
  on the same data (e.g. older and newer versions of your editor,
  a different set of plugins based on context, etc.).
- Pre-registration system for nodes to declare expected state
  and serialize them as top-level properties (`flat`) with `$config`
  (see [#7260](https://github.com/facebook/lexical/issues/7260)).
- Can be integrated with
  [DOMRenderExtension](../serialization/dom-render.md) for editor
  rendering and HTML export
- Can be integrated with
  [DOMImportExtension](../serialization/dom-import.md) for HTML import

Future:

- Does not yet support direct integration with Yjs, e.g.
  you can not store a Y.Map as a NodeState value
  (see [#7293](https://github.com/facebook/lexical/issues/7293))

## Node State Style Example

This example demonstrates an advanced use case of storing a style object on TextNode using NodeState.

[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-state-style?file=src/main.tsx)

<iframe width="100%" height="600" src="https://stackblitz.com/github/facebook/lexical/tree/main/examples/node-state-style?embed=1&file=src%2FApp.tsx&terminalHeight=0&ctl=1&showSidebar=0&devtoolsheight=0&view=preview" sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts" title="Node State Style Example"></iframe>

---

### Packages Lexical Website Docs Concepts Nodes (`packages/lexical-website/docs/concepts/nodes.mdx`)

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Nodes

## Base Nodes

Nodes are a core concept in Lexical. Not only do they form the visual editor view, as part of the `EditorState`, but they also represent the
underlying data model for what is stored in the editor at any given time. Lexical has a single core based node, called `LexicalNode` that
is extended internally to create Lexical's five base nodes:

- `RootNode`
- `LineBreakNode`
- `ElementNode`
- `TextNode`
- `DecoratorNode`

Of these base nodes, three of them can be extended to create new types of nodes:

- `ElementNode`
- `TextNode`
- `DecoratorNode`

### [`RootNode`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/nodes/LexicalRootNode.ts)

There is only ever a single `RootNode` in an `EditorState` and it is always at the top and it represents the
`contenteditable` itself. This means that the `RootNode` does not have a parent or siblings. It can not be
subclassed or replaced.

- To get the text content of the entire editor, you should use `rootNode.getTextContent()`.
- To avoid selection issues, Lexical forbids insertion of text nodes directly into a `RootNode`.

#### Semantics and Use Cases

Unlike other `ElementNode` subclasses, the `RootNode` has specific characteristics and restrictions to maintain editor integrity:

1. **Non-extensibility**  
   The `RootNode` cannot be subclassed or replaced with a custom implementation. It is designed as a fixed part of the editor architecture.

2. **Exclusion from Mutation Listeners**  
   The `RootNode` does not participate in mutation listeners. Instead, use a root-level or update listener to observe changes at the document level.

3. **Compatibility with Node Transforms**  
   While the `RootNode` is not "part of the document" in the traditional sense, it can still appear to be in some cases, such as during serialization or when applying node transforms. A node transform on the `RootNode` will be called at the end of _every_ node transform cycle. This is useful in cases where you need something like an update listener that occurs before the editor state is reconciled.

4. **Document-Level Metadata**  
   If you are attempting to use the `RootNode` for document-level metadata (e.g., undo/redo support), use the [NodeState](/docs/concepts/node-state) API.

By design, the `RootNode` serves as a container for the editor's content rather than an active part of the document's logical structure. This approach simplifies operations like serialization and keeps the focus on content nodes.

### [`LineBreakNode`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/nodes/LexicalLineBreakNode.ts)

You should never have `'\n'` in your text nodes, instead you should use the `LineBreakNode` which represents
`'\n'`, and more importantly, can work consistently between browsers and operating systems.

### [`ElementNode`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/nodes/LexicalElementNode.ts)

Used as parent for other nodes, can be block level (`ParagraphNode`, `HeadingNode`) or inline (`LinkNode`).
Has various methods which define its behaviour that can be overridden during extension (`isInline`, `canBeEmpty`, `canInsertTextBefore` and more)

### [`TextNode`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/nodes/LexicalTextNode.ts)

Leaf type of node that contains text. It also includes few text-specific properties:

- `format` any combination of `bold`, `italic`, `underline`, `strikethrough`, `code`, `highlight`, `subscript`, `superscript`, `lowercase`, `uppercase` and `capitalize`
- `mode`
  - `token` - acts as immutable node, can't change its content and is deleted all at once
  - `segmented` - its content deleted by segments (one word at a time), it is editable although node becomes non-segmented once its content is updated
- `style` can be used to apply inline css styles to text

### [`DecoratorNode`](https://github.com/facebook/lexical/blob/main/packages/lexical/src/nodes/LexicalDecoratorNode.ts)

Wrapper node to insert arbitrary view (component) inside the editor. Decorator node rendering is framework-agnostic and
can output components from React, vanilla js or other frameworks.
See [Decorators](/docs/concepts/decorators) for guidance on when and how to use them.

## Node Properties

A custom node stores its own properties as fields on the instance. By
convention the field names start with `__` (double underscore) to show that
they are private and should not be accessed directly. We use `__` instead of
`_` because some build tooling mangles and minifies single `_` prefixed
properties to improve code size, which breaks down if you're exposing a node
to be extended outside of your build.

In Lexical v0.51.0 and later, declare each property in the node's `$config`
with a `json`
[serialization schema](#creating-custom-nodes-with-a-serialization-schema).
The schema states the property's JSON name, the field it is stored in, and the
values it accepts, and from that Lexical implements `exportJSON`,
`updateFromJSON`, `importJSON`, `clone`, and `afterCloneFrom` for you. Values
must be JSON serializable: `null`, `number`, `string`, `boolean`, and plain
objects and arrays of those.

If a property should be readable or changeable from outside the node, give it
`get*()` and `set*()` methods. They must call `getLatest()` and
`getWritable()`, which keep the node consistent with Lexical's immutable
editor state.

```ts
import {ElementNode, nodeSchema, stringValue, withField} from 'lexical';

const myCustomNodeSchema = nodeSchema<MyCustomNode>()({
  foo: withField(stringValue(), {field: '__foo'}),
});

class MyCustomNode extends ElementNode {
  __foo: string = '';

  $config() {
    return this.config('my-custom', {
      extends: ElementNode,
      json: myCustomNodeSchema,
    });
  }

  setFoo(foo: string): this {
    // getWritable() creates a clone of the node
    // if needed, to ensure we don't try and mutate
    // a stale version of this node.
    const self = this.getWritable();
    self.__foo = foo;
    return self;
  }

  getFoo(): string {
    // getLatest() ensures we are getting the most
    // up-to-date value from the EditorState.
    const self = this.getLatest();
    return self.__foo;
  }
}
```

The constructor must support being called with no arguments, which `$config`,
`clone`, and collaboration rely on. Give every field a default, as `__foo`
has above, rather than declaring it optional (`__foo?: string`): a
freshly constructed instance needs the property to exist for
[collaboration](/docs/collaboration/react#custom-node-property-syncing) to sync
it. You can still define your `$create*` functions with required arguments.

:::tip

To add a property to a node class you don't own, such as `ParagraphNode`, or
to attach ad-hoc data to any node without writing a subclass, use
[NodeState](/docs/concepts/node-state) instead.

:::

### Legacy: node properties without a serialization schema

Before v0.51.0, `$config` only implemented `clone` and `importJSON`. A node
with its own fields also has to implement `exportJSON`, `updateFromJSON`, and
`afterCloneFrom` by hand, and each one has to call its `super` method:

```ts
import {
  ElementNode,
  type LexicalUpdateJSON,
  type SerializedElementNode,
} from 'lexical';

interface SerializedMyCustomNode extends SerializedElementNode {
  foo?: string;
}

class MyCustomNode extends ElementNode {
  __foo: string = '';

  $config() {
    return this.config('my-custom', {extends: ElementNode});
  }

  afterCloneFrom(prevNode: this): void {
    super.afterCloneFrom(prevNode);
    this.__foo = prevNode.__foo;
  }

  updateFromJSON(
    serializedNode: LexicalUpdateJSON<SerializedMyCustomNode>,
  ): this {
    const self = super.updateFromJSON(serializedNode);
    return typeof serializedNode.foo === 'string'
      ? self.setFoo(serializedNode.foo)
      : self;
  }

  exportJSON(): SerializedMyCustomNode {
    const serializedNode: SerializedMyCustomNode = super.exportJSON();
    const foo = this.getFoo();
    if (foo !== '') {
      serializedNode.foo = foo;
    }
    return serializedNode;
  }

  // setFoo() and getFoo() as above
}
```

For nodes written without `$config` at all, see [Legacy: custom nodes without `$config`](#legacy-custom-nodes-without-config).

## Creating custom nodes with a serialization schema

:::caution Experimental

This feature became available in the lexical v0.51.0 release and is still
considered experimental. Its implementation may change in any release
without a deprecation period.

:::

A node's properties have to be propagated three times: exported to JSON,
imported from untrusted JSON, and carried across a clone.
`$config`'s `json` option replaces all three with a declarative schema.

```ts
import {
  ElementNode,
  enumValue,
  nodeSchema,
  numberValue,
  stringValue,
  withField,
} from 'lexical';

const calloutNodeSchema = nodeSchema<CalloutNode>()({
  // Each property *is* a field: exported by reading it, imported by assigning
  // it, with no method call on either side.
  label: withField(stringValue(), {field: '__label'}),
  level: withField(numberValue(1, {integer: true, max: 3, min: 1}), {
    field: '__level',
  }),
  tone: withField(enumValue(['info', 'warning', 'danger']), {field: '__tone'}),
});

export class CalloutNode extends ElementNode {
  __label: string = '';
  __level: number = 1;
  __tone: 'info' | 'warning' | 'danger' = 'info';

  $config() {
    return this.config('callout', {
      extends: ElementNode,
      // highlight-next-line
      json: calloutNodeSchema,
    });
  }

  createDOM(): HTMLElement {
    return document.createElement('aside');
  }

  updateDOM(): false {
    return false;
  }
}
```

Stating the domain (all possible values the property may inhabit) as data
is the prerequisite that enables all of the new features. In the initial
release, we have:

- **Safer JSON import** - any missing or invalid properties are parsed as
  the default value, instead of potentially silently corrupting the node's
  state
- **Less boilerplate** - all of the boilerplate for `exportJSON`,
  `updateFromJSON` and `afterCloneFrom` is handled automatically. Declaring a
  property as a field says where it is stored, which is exactly what a clone
  has to carry; a property declared through accessor methods names no field, so
  a node with one of those still writes its own `afterCloneFrom`. See
  [Carrying properties across a clone](/docs/serialization/#carrying-properties-across-a-clone).
- **Performance** - a `@lexical/compiler` code generator is used to produce
  optimized implementations of export, import and clone for performance that is
  indistinguishable (or better) than a hand-written implementation.
- **Property Tests** - the `@lexical/fast-check` package can be used to
  generate arbitrary valid serialized nodes for thorough testing of your
  custom nodes. It's never been easier to find edge cases, automatically.

In the future, this declarative schema can be used to assist in supporting
additional protocols, improving developer tools, porting to other
language/platforms, etc.

The combinators (`stringValue`, `numberValue`, `enumValue`, `nullable`,
`optional`, `arrayValue`, `unionValue`, `withField`, `withAccessors`, …) and
the rules for properties that are not plain fields are covered in
[Serialization & Deserialization](/docs/serialization/#declarative-serialization-schemas-with-config).

For performance reasons, you should prefer serialization schema over NodeState
when the property is inherent to the node type and not ad-hoc.

### Backwards compatible return types

That declaration is the whole of the node's serialization. `exportJSON`,
`updateFromJSON` and `afterCloneFrom` are all derived from it.

Their types stay the ones the node inherits: `exportJSON()` is typed as
`SerializedElementNode` here, whatever it actually writes.

Declaration merging can be used to provide a backwards compatible type
interface, for nodes where the `exportJSON()` return type might have been
treated as a public API:

```ts
export type SerializedCalloutNode = Spread<
  {label: string; level: number; tone: 'info' | 'warning' | 'danger'},
  SerializedElementNode
>;

// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export interface CalloutNode {
  exportJSON(compact?: false): SerializedCalloutNode;
  exportJSON(compact: boolean): SerializedPartial<SerializedCalloutNode>;
  updateFromJSON(serializedNode: LexicalParseJSON<SerializedCalloutNode>): this;
}
```

For backwards compatibility, these interfaces are provided for Lexical's
exported node classes. We do not recommend maintaining this type boilerplate
unless you have code that requires it.

## Creating custom nodes with $config and NodeState

In Lexical v0.33.0, a new method for defining custom nodes was added to reduce
boilerplate and add features used by the
[NodeState](/docs/concepts/node-state) API.

The following section shows how the previous examples would be refactored to
use the latest functionality, reducing boilerplate.

Note that since these example use NodeState and `$config`, they will
automatically get full and correct implementations of the following methods:

- `static clone`
- `static importJSON`
- `updateFromJSON`
- `afterCloneFrom`
- `exportJSON`

### Best Practices

- The constructor of any custom node must have zero required arguments.
  This is required for `@lexical/yjs` support, `$create` support, and allows the
  boilerplate static `clone` and `importJSON` methods to be eliminated.
  - ✅ `constructor(text: string = '', key?: NodeKey)`
  - ❌ `constructor(text: string, key?: NodeKey)`

- Declaring the node's own properties with a `json`
  [serialization schema](#creating-custom-nodes-with-a-serialization-schema),
  or storing ad-hoc data in NodeState, allows the boilerplate
  `afterCloneFrom`, `exportJSON`, and `updateFromJSON` methods to be
  eliminated.

### Extending `ElementNode` with $config

Below is an example of how you might extend `ElementNode`:

<Tabs>
<TabItem value="$config" label="Using $config">

```ts
import {
  $create,
  type EditorConfig,
  ElementNode,
  type LexicalNode,
} from 'lexical';

export class CustomParagraph extends ElementNode {
  // highlight-start
  $config() {
    return this.config('custom-paragraph', {extends: ElementNode});
  }
  // highlight-end

  createDOM(): HTMLElement {
    // Define the DOM element here
    const dom = document.createElement('p');
    return dom;
  }

  updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean {
    // Returning false tells Lexical that this node does not need its
    // DOM element replaced with a new one from createDOM.
    return false;
  }
}
```

</TabItem>
<TabItem value="legacy" label="Legacy static methods">

```ts
import {
  $create,
  type EditorConfig,
  ElementNode,
  type LexicalNode,
  // highlight-next-line
  type SerializedLexicalNode,
} from 'lexical';

export class CustomParagraph extends ElementNode {
  // highlight-start
  static getType(): string {
    return 'custom-paragraph';
  }

  static clone(node: CustomParagraph): CustomParagraph {
    return new CustomParagraph(node.__key);
  }

  static importJSON(serializedNode: SerializedLexicalNode): CustomParagraph {
    return new CustomParagraph().updateFromJSON(serializedNode);
  }
  // highlight-end

  createDOM(): HTMLElement {
    // Define the DOM element here
    const dom = document.createElement('p');
    return dom;
  }

  updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean {
    // Returning false tells Lexical that this node does not need its
    // DOM element replacing with a new copy from createDOM.
    return false;
  }
}
```

</TabItem>
</Tabs>

It's also good etiquette to provide some `$` prefixed utility functions for
your custom `ElementNode` so that others can easily consume and validate nodes
are that of your custom node. Here's how you might do this for the above example:

```js
export function $createCustomParagraphNode(): CustomParagraph {
  return $create(CustomParagraph);
}

export function $isCustomParagraphNode(
  node: LexicalNode | null | undefined
): node is CustomParagraph  {
  return node instanceof CustomParagraph;
}
```

### Extending `TextNode` with $config

<Tabs>
<TabItem value="$config" label="Using $config">

```
/* Detailed source-code truncated for AI context efficiency. */
```

</TabItem>
<TabItem value="legacy" label="Legacy static methods and properties">

```
/* Detailed source-code truncated for AI context efficiency. */
```

</TabItem>
</Tabs>

### Extending `DecoratorNode` using $config

<Tabs>
<TabItem value="$config" label="Using $config">

```ts
import {
  // highlight-start
  $create,
  $getState,
  $getStateChange,
  $setState,
  // highlight-end
  DecoratorNode,
  type EditorConfig,
  type LexicalNode,
  // highlight-next-line
  createState,
} from 'lexical';

// highlight-start
const idState = createState('id', {
  parse: (value) => (typeof value === 'string' ? value : ''),
});
// highlight-end

export class VideoNode extends DecoratorNode<ReactNode> {
  // highlight-start
  $config() {
    return this.config('video', {
      extends: DecoratorNode,
      stateConfigs: [{flat: true, stateConfig: idState}],
    });
  }
  // highlight-end

  createDOM(): HTMLElement {
    return document.createElement('div');
  }

  updateDOM(): false {
    return false;
  }

  decorate(): ReactNode {
    // highlight-next-line
    return <VideoPlayer videoID={$getState(this, idState)} />;
  }
}

export function $createVideoNode(id: string): VideoNode {
  // highlight-next-line
  return $setState($create(VideoNode), idState, id);
}

export function $isVideoNode(
  node: LexicalNode | null | undefined,
): node is VideoNode {
  return node instanceof VideoNode;
}
```

</TabItem>
<TabItem value="legacy" label="Legacy static methods and properties">

```ts
import {
  DecoratorNode,
  type EditorConfig,
  type LexicalNode,
  // highlight-start
  type SerializedLexicalNode,
  type Spread,
  // highlight-end
} from 'lexical';

// highlight-start
export type SerializedVideoNode = Spread<
  {
    id: string;
  },
  SerializedLexicalNode
>;
// highlight-end

export class VideoNode extends DecoratorNode<ReactNode> {
  // highlight-start
  __id: string;

  static getType(): string {
    return 'video';
  }

  static clone(node: VideoNode): VideoNode {
    return new VideoNode(node.__id, node.__key);
  }

  static importJSON(serializedNode: SerializedVideoNode): VideoNode {
    return new VideoNode(serializedNode.id).updateFromJSON(serializedNode);
  }

  constructor(id: string, key?: NodeKey) {
    super(key);
    this.__id = id;
  }

  exportJSON(): SerializedVideoNode {
    return {...super.exportJSON(), id: this.__id};
  }
  // highlight-end

  createDOM(): HTMLElement {
    return document.createElement('div');
  }

  updateDOM(): false {
    return false;
  }

  decorate(): ReactNode {
    return <VideoPlayer videoID={this.__id} />;
  }
}

export function $createVideoNode(id: string): VideoNode {
  // highlight-next-line
  return $applyNodeReplacement(new VideoNode(id));
}

export function $isVideoNode(
  node: LexicalNode | null | undefined,
): node is VideoNode {
  return node instanceof VideoNode;
}
```

</TabItem>
</Tabs>

In React, the editor rendered by `LexicalExtensionComposer` (and, in legacy setups, `RichTextPlugin` or `PlainTextPlugin`) calls `React.createPortal(reactDecorator, element)` for each `DecoratorNode`,
where the `reactDecorator` is what is returned by `DecoratorNode.prototype.decorate`,
and the `element` is an `HTMLElement` returned by `DecoratorNode.prototype.createDOM`.

### Sharing `$config` across an abstract base class

A concrete node records its string `type` in `$config`. An abstract base class
has no concrete `type` of its own, but it can still declare configuration that
is shared with all of its concrete subclasses — for example a `$transform`, or
required [NodeState](/docs/concepts/node-state) — by keying its `$config` with a
well-known symbol instead of a string. By convention this is
`Symbol.for(<ClassName>)`:

```ts
import {ElementNode} from 'lexical';

// Abstract base — it has no concrete node `type` of its own.
export class BaseBoxNode extends ElementNode {
  $config() {
    return this.config(Symbol.for('BaseBoxNode'), {
      extends: ElementNode,
      // Registered for this class and every subclass.
      $transform: (node: BaseBoxNode): void => {
        // ...
      },
    });
  }
}

// Concrete subclass — inherits the shared $transform.
export class RedBoxNode extends BaseBoxNode {
  $config() {
    return this.config('red-box', {extends: BaseBoxNode});
  }

  createDOM(): HTMLElement {
    return document.createElement('div');
  }

  updateDOM(): false {
    return false;
  }
}
```

The descriptive, globally-registered symbol keeps the configuration easy to find
in a debugger and can never collide with a real node `type`.

### Nominal typing for structurally-identical nodes

TypeScript's type system is _structural_: two classes with the same members are
mutually assignable even when one is declared as a subclass of the other. So a
node subclass that adds no type-distinguishable members over its base — for
example a node that only overrides methods with identical signatures — is
structurally identical to its base, despite being a distinct class. The built-in
`TabNode` (which extends `TextNode`) is one such node. Because the two are
structurally identical, TypeScript considers `TextNode` assignable to `TabNode`,
so without a nominal distinction the negative branch of an `instanceof`-style
type guard like `$isTabNode()` would collapse to `never`:

```ts
import {$isTabNode, type TextNode} from 'lexical';

function $example(node: TextNode) {
  if ($isTabNode(node)) {
    return;
  }
  // `node` is correctly narrowed to `TextNode` here.
  node.getFormat();
}
```

You get this distinction automatically by declaring the superclass with
`extends` in `$config` — which is exactly what `TabNode` does:

```ts
import {TextNode} from 'lexical';

export class TabNode extends TextNode {
  $config() {
    // highlight-next-line
    return this.config('tab', {extends: TextNode});
  }
}
```

Lexical records each node's own `type` in the `$config` return type along the
`extends` chain. That makes the classes nominally distinct: the base is no
longer assignable to the subclass (so the guard narrows correctly) while the
subclass remains assignable to the base. It works at every level of the
hierarchy — a subclass of `TabNode` is likewise distinct from `TabNode` — it is
type-only with no runtime cost, and there is nothing to declare on the class
beyond the `extends` you already provide.

## Legacy: custom nodes without `$config`

This is how custom nodes were written before `$config` (Lexical v0.33.0). It still works, but is maintained only for backwards compatibility, and the legacy interface may be removed before Lexical 1.0, so prefer the sections above for new code. Every one of these methods is something `$config` writes for you.

As mentioned above, Lexical exposes three base nodes that can be extended.

> Did you know? Nodes such as `ElementNode` are already extended in the core by Lexical, such as `ParagraphNode` and `RootNode`!

### Extending `ElementNode`

Below is an example of how you might extend `ElementNode`:

```ts
import {ElementNode, LexicalNode} from 'lexical';

export class CustomParagraph extends ElementNode {
  static getType(): string {
    return 'custom-paragraph';
  }

  static clone(node: CustomParagraph): CustomParagraph {
    return new CustomParagraph(node.__key);
  }

  createDOM(): HTMLElement {
    // Define the DOM element here
    const dom = document.createElement('p');
    return dom;
  }

  updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean {
    // Returning false tells Lexical that this node does not need its
    // DOM element replacing with a new copy from createDOM.
    return false;
  }
}
```

It's also good etiquette to provide some `$` prefixed utility functions for
your custom `ElementNode` so that others can easily consume and validate nodes
are that of your custom node. Here's how you might do this for the above example:

```js
export function $createCustomParagraphNode(): CustomParagraph {
  return $applyNodeReplacement(new CustomParagraph());
}

export function $isCustomParagraphNode(
  node: LexicalNode | null | undefined
): node is CustomParagraph  {
  return node instanceof CustomParagraph;
}
```

### Extending `TextNode`

```ts
export class ColoredNode extends TextNode {
  __color: string;

  constructor(text: string, color: string, key?: NodeKey) {
    super(text, key);
    this.__color = color;
  }

  static getType(): string {
    return 'colored';
  }

  static clone(node: ColoredNode): ColoredNode {
    return new ColoredNode(node.__text, node.__color, node.__key);
  }

  createDOM(config: EditorConfig): HTMLElement {
    const element = super.createDOM(config);
    element.style.color = this.__color;
    return element;
  }

  updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean {
    const isUpdated = super.updateDOM(prevNode, dom, config);
    if (prevNode.__color !== this.__color) {
      dom.style.color = this.__color;
    }
    return isUpdated;
  }
}

export function $createColoredNode(text: string, color: string): ColoredNode {
  return $applyNodeReplacement(new ColoredNode(text, color));
}

export function $isColoredNode(
  node: LexicalNode | null | undefined,
): node is ColoredNode {
  return node instanceof ColoredNode;
}
```

### Extending `DecoratorNode`

```ts
export class VideoNode extends DecoratorNode<ReactNode> {
  __id: string;

  static getType(): string {
    return 'video';
  }

  static clone(node: VideoNode): VideoNode {
    return new VideoNode(node.__id, node.__key);
  }

  constructor(id: string, key?: NodeKey) {
    super(key);
    this.__id = id;
  }

  createDOM(): HTMLElement {
    return document.createElement('div');
  }

  updateDOM(): false {
    return false;
  }

  decorate(): ReactNode {
    return <VideoPlayer videoID={this.__id} />;
  }
}

export function $createVideoNode(id: string): VideoNode {
  return $applyNodeReplacement(new VideoNode(id));
}

export function $isVideoNode(
  node: LexicalNode | null | undefined,
): node is VideoNode {
  return node instanceof VideoNode;
}
```

In React, the editor rendered by `LexicalExtensionComposer` (and, in legacy setups, `RichTextPlugin` or `PlainTextPlugin`) calls `React.createPortal(reactDecorator, element)` for each `DecoratorNode`,
where the `reactDecorator` is what is returned by `DecoratorNode.prototype.decorate`,
and the `element` is an `HTMLElement` returned by `DecoratorNode.prototype.createDOM`.

### The rest of the boilerplate

When using this method of extension, it is also required to implement the
following methods:

- `static clone` (always - this is already in the above examples)
- `static importJSON` (always)
- `updateFromJSON` (if any custom properties are defined)
- `afterCloneFrom` (if any custom properties are defined that are not carried over from static clone)
- `exportJSON` (if any custom properties are defined)

Without `$config`, all nodes must have `static getType()`, `static clone()`, and `static importJSON()` methods.
Lexical uses the type to be able to reconstruct a node back with its associated class prototype
during deserialization (important for copy + paste!). Lexical uses cloning to ensure consistency
between creation of new `EditorState` snapshots.

Here is a node with one custom property and all of these methods written by hand:

```
/* Detailed source-code truncated for AI context efficiency. */
```

---

### Packages Lexical Website Docs Concepts One Lexical Per App (`packages/lexical-website/docs/concepts/one-lexical-per-app.md`)

---
sidebar_label: One Lexical per app
---

# One Lexical per app

`lexical` is effectively a singleton **within one app**: an app must resolve
exactly **one** copy of `lexical`, and one copy of each `@lexical/*` package it
uses. Two copies in the same bundle do not merely waste bytes — they break the
editor in ways whose error messages point nowhere near the real cause.

The practical consequence, covered in [Publishing a library](#publishing-a-library)
below: **a library built on Lexical declares `lexical` and the `@lexical/*`
packages it imports as `peerDependencies`, never as `dependencies`. The
application brings its own Lexical.**

:::note

This page is about npm `peerDependencies` in your `package.json`. Extensions
have their own, unrelated notion of
[peer dependencies](../extensions/peer-dependencies.md) — optional links between
extensions inside one editor.

:::

## The boundary is the app, not the page

Several self-contained apps can share a page, each with its own bundled copy of
Lexical, and that is fine — a CMS page with two independently deployed widgets
on it, or a micro-frontend setup, does not have a bug just because two copies of
`lexical` are loaded. Lexical anticipates this: `isLexicalEditor()` is an
`instanceof` check specifically "to prevent issues with multiple embedded
Lexical installations", and an editor is tagged on its own root element rather
than in any page-global registry, so each app's editors resolve to that app's
Lexical and ignore the others.

What those apps cannot do is interoperate. Two copies have no compatible API
surface between them, so treat each as a sealed island:

- do not pass nodes, editors, `EditorState` objects, selections or commands
  across the boundary — pass serialized JSON or HTML, which is data rather than
  identity;
- a plugin, node package or shared editor config must be bundled with the app it
  serves, not shared as a live import between apps;
- `@lexical/react` context does not cross the boundary either, since the context
  object itself comes from one copy.

The rest of this page is about the failure case: two copies **inside one app**,
where code from copy A is expected to work with an editor from copy B.

## Why one copy per app

Lexical keeps state at module scope, and identifies things by reference rather
than by name. Both assumptions hold only within a single copy of the module.

- **The active editor is module state.** `activeEditor`, `activeEditorState`
  and `isReadOnlyMode` live at module scope in `LexicalUpdates.ts`, and every
  `$` function reads them. `editor.update()` in copy A sets copy A's variables;
  a `$` function imported from copy B reads copy B's, which are still `null`
  ([Editor State](editor-state.md)).
- **Node classes are compared by identity.** Node registration and the
  `instanceof LexicalNode` checks compare class references. A node that extends
  `LexicalNode` from copy A is not, as far as copy B is concerned, a
  `LexicalNode` at all — even though both classes have the same name and come
  from the same published version ([Nodes](nodes.mdx)).
- **Commands are object identities.** `createCommand()` returns a fresh `{type}`
  object, and `registerCommand`/`dispatchCommand` match on that object, not on
  its `type` string. Two copies of `lexical` export two distinct
  `FORMAT_TEXT_COMMAND` objects, so a dispatch from one never reaches a listener
  registered with the other ([Commands](commands.md)).

On top of that, two copies are usually two *versions*, which is API drift inside
a single editor: half your plugins were compiled against a different API and
serialization format than the editor running them, and the mismatch surfaces
only at runtime, on the paths that happen to differ.

## Symptoms of a duplicated Lexical

The errors are unhelpfully literal — a class that plainly does implement
`getType()` is reported as not implementing it, because the check is running
against the other copy:

```
LexicalNode: Node FooNode does not implement .getType().

Create node: Type heading in node HeadingNode does not match registered node
HeadingNode with the same type

createEditor: nodes[0] FooNode (type foo) is not a constructor that subclasses
LexicalNode from the lexical package used by this editor (0.50.0)

Unable to find an active editor state. State helpers or node methods can only be
used synchronously during the callback of editor.update(), editor.read(), or
editorState.read(). Detected on the page: 0 compatible editor(s) with version
0.50.0 and incompatible editors with versions 0.49.0
```

That last sentence is Lexical telling you directly that it found editors built
from another copy — a version different from its own, or the same version marked
`(separately built, likely a bundler configuration issue)`. Note that this
diagnostic scans the whole page, so a genuinely separate app that ships its own
Lexical is counted there too; it is evidence of a bug only when those editors
were supposed to be part of *your* app.

Silent failures look like: a command that dispatches but never fires its
listener, `$getSelection()` returning `null` inside an update, or a custom node
rendering as plain text after a copy/paste. Real reports of this failure mode:
[#4225](https://github.com/facebook/lexical/issues/4225) and
[#7819](https://github.com/facebook/lexical/issues/7819).

## Publishing a library

If you publish a package that imports from `lexical` or `@lexical/*` — a plugin,
a node package, a shared editor config, an in-house wrapper in a monorepo — put
those packages in `peerDependencies`, and in `devDependencies` so you can still
build and test:

```json
{
  "name": "my-lexical-plugin",
  "peerDependencies": {
    "lexical": ">=0.50.0",
    "@lexical/react": ">=0.50.0",
    "@lexical/utils": ">=0.50.0"
  },
  "devDependencies": {
    "lexical": "0.50.0",
    "@lexical/react": "0.50.0",
    "@lexical/utils": "0.50.0"
  }
}
```

Notes on the ranges:

- Lexical is pre-1.0, so `^0.50.0` means `>=0.50.0 <0.51.0` — a caret peer range
  locks your users out of the next release. Prefer an open `>=` range (or an
  explicit `>=0.50.0 <1.0.0`) so an app can upgrade Lexical without waiting for
  you.
- Mark a peer optional with
  [`peerDependenciesMeta`](https://docs.npmjs.com/cli/configuring-npm/package-json#peerdependenciesmeta)
  when the import is behind an opt-in entry point, the way this repo's own
  packages mark `typescript` optional.
- Do not add `lexical` to `dependencies` "just so it installs". That is exactly
  what pins a second copy under `node_modules/your-lib/node_modules/lexical` as
  soon as your range and the app's range disagree.

## Application setup

The application owns the version, and every Lexical package it installs moves
together:

- Install `lexical` and each `@lexical/*` package you use **at the same
  version**. Published `@lexical/*` packages depend on the exact matching
  `lexical` version (the monorepo's `workspace:*` is rewritten to an exact
  version at publish time), so `@lexical/react@0.50.0` beside `lexical@0.49.0`
  guarantees a nested second copy.
- Verify there is only one **per app**, from the app root:

  ```sh
  npm ls lexical @lexical/react     # or: pnpm why lexical / yarn why lexical
  ```

  More than one resolved version, or a path with a nested `node_modules/lexical`
  in it, is the bug.
- In a monorepo or a linked-package setup (`npm link`, `pnpm link`, a Yalc
  copy), the linked library resolves Lexical from *its own* `node_modules` and
  duplicates are the default rather than the exception. Deduplicate explicitly:
  hoist the dependency to the workspace root, or force resolution with
  [pnpm `overrides`](https://pnpm.io/settings#overrides), Yarn `resolutions`, or
  `npm dedupe`.
- Bundlers can also be told to collapse duplicates — Vite
  [`resolve.dedupe`](https://vite.dev/config/shared-options.html#resolve-dedupe),
  webpack `resolve.alias` pointing each package at one path:

  ```js
  // vite.config.js
  export default {
    resolve: {dedupe: ['lexical', '@lexical/react', '@lexical/utils']},
  };
  ```

  Treat that as a safety net, not a fix: it collapses copies that are already
  version-compatible, and cannot rescue two genuinely different versions.
- Duplication does not always come from the dependency tree: importing Lexical
  through both `import` and `require` pulls in the ESM and the CJS build of the
  *same* version, which are still two copies. The
  [React FAQ](../react/faq.md#lexicalcomposercontextuselexicalcomposercontext-cannot-find-a-lexicalcomposercontext)
  covers how this one shows up in `@lexical/react`.

---

### Packages Lexical Website Docs Concepts Read Only (`packages/lexical-website/docs/concepts/read-only.md`)

# Read Mode / Edit Mode

Lexical supports two modes:

- Read mode
- Edit mode

The default behavior for Lexical is edit mode, or more accurately not read only mode. Under-the-hood, the main
implementation detail is that the `contentEditable` is being set to `"false"` or `"true"` depending on the mode.
Specific plugins can listen to the mode change too – allowing them to customize parts of the UI depending on the
mode.

## Setting the mode

In order to set the mode, this can be done on creation of the editor, with the
`editable` property of your app's extension:

```js
import {buildEditorFromExtensions} from '@lexical/extension';
import {RichTextExtension} from '@lexical/rich-text';
import {defineExtension} from 'lexical';

const editor = buildEditorFromExtensions(
  defineExtension({
    dependencies: [RichTextExtension],
    editable: false,
    name: '@my-app/editor',
  }),
);
```

With React, pass the same extension to `<LexicalExtensionComposer>`:

```jsx
<LexicalExtensionComposer extension={appExtension}>
  ...
</LexicalExtensionComposer>
```

After an editor is created, the mode can be changed imperatively:

```js
editor.setEditable(true);
```

## Reading the mode

In order to find the current mode of the editor you can use:

```js
const isEditable = editor.isEditable(); // Returns true or false
```

You can also get notified when the editor's read only mode has changed:

```js
const removeEditableListener = editor.registerEditableListener(
  (isEditable) => {
    // The editor's mode is passed in!
    console.log(isEditable);
  },
);

// Do not forget to unregister the listener when no longer needed!
removeEditableListener();
```

In an extension, depend on
[`WatchEditableExtension`](/docs/api/modules/lexical_extension#watcheditableextension),
whose output is a signal holding the current editable state, instead of
registering the listener yourself.

---

### Packages Lexical Website Docs Concepts Selection (`packages/lexical-website/docs/concepts/selection.md`)

# Selection

## Types of selection

Lexical's selection is part of the `EditorState`. This means that for every update, or change to the editor, the
selection always remains consistent with that of the `EditorState`'s node tree.

In Lexical, there are four types of selection possible:

- `RangeSelection`
- `NodeSelection`
- `TableSelection` (implemented in `@lexical/table`)
- `null`

It is possible, but not generally recommended, to implement your own selection types that implement `BaseSelection`.

### `RangeSelection`

This is the most common type of selection, and is a normalization of the browser's DOM Selection and Range APIs.
`RangeSelection` consists of three main properties:

- `anchor` representing a `RangeSelection` point
- `focus` representing a `RangeSelection` point
- `format` numeric bitwise flag, representing any active text formats

Both the `anchor` and `focus` points refer to an object that represents a specific part of the editor. The main properties of a `RangeSelection` point are:

- `key` representing the `NodeKey` of the selected Lexical node
- `offset` representing the position from within its selected Lexical node. For the `text` type this is the character, and for the `element` type this is the child index from within the `ElementNode`
- `type` representing either `element` or `text`.

### `NodeSelection`

NodeSelection represents a selection of multiple arbitrary nodes. For example, three images selected at the same time.

- `getNodes()` returns an array containing the selected LexicalNodes

### `TableSelection`

TableSelection represents a grid-like selection like tables. It stores the key of the parent node where the selection takes place and the start and end points.
`TableSelection` consists of three main properties:

- `tableKey` representing the parent node key where the selection takes place
- `anchor` representing a `TableSelection` point
- `focus` representing a `TableSelection` point

For example, a table where you select row = 1 col = 1 to row 2 col = 2 could be stored as follows:
- `tableKey = 2` table key
- `anchor = 4` table cell (key may vary)
- `focus = 10` table cell (key may vary)

Note that `anchor` and `focus` points work the same way as `RangeSelection`.

### `null`

This is for when the editor doesn't have any active selection. This is common for when the editor has been blurred or when selection
has moved to another editor on the page. This can also happen when trying to select non-editable components within the editor space.

## Working with selection

Selection can be found using the `$getSelection()` helper, exported from the `lexical` package. This function can be used within
an update, a read, or a command listener.

:::tip

To walk the document from a selection point — or to write a traversal that
handles empty nodes and collapsed selections correctly — see
[Node Traversals with NodeCaret](./traversals.md).
`$caretRangeFromSelection(selection)` converts a `RangeSelection` into the
caret range that traversal API works with, and
`$setSelectionFromCaretRange(range)` converts back.

:::

```js
import {$getSelection, SELECTION_CHANGE_COMMAND} from 'lexical';

editor.update(() => {
  const selection = $getSelection();
});

editorState.read(() => {
  const selection = $getSelection();
});

// SELECTION_CHANGE_COMMAND runs before reconciliation in the pending update.
editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
  const selection = $getSelection();
});
```

In some cases you might want to create a new type of selection and set the editor selection to
be that. This can only be done in update or command listeners.

```js
import {$setSelection, $createRangeSelection, $createNodeSelection} from 'lexical';

editor.update(() => {
  // Set a range selection
  const rangeSelection = $createRangeSelection();
  $setSelection(rangeSelection);

  // You can also indirectly create a range selection, by calling some of the selection
  // methods on Lexical nodes.
  const someNode = $getNodeByKey(someKey);

  // On element nodes, this will create a RangeSelection with type "element",
  // referencing an offset relating to the child within the element.
  // On text nodes, this will create a RangeSelection with type "text",
  // referencing the text character offset.
  someNode.select();
  someNode.selectPrevious();
  someNode.selectNext();

  // You can use this on any node.
  someNode.selectStart();
  someNode.selectEnd();

  // Set a node selection
  const nodeSelection = $createNodeSelection();
  // Add a node key to the selection.
  nodeSelection.add(someKey);
  $setSelection(nodeSelection);

  // You can also clear selection by setting it to `null`.
  $setSelection(null);
});
```

## Focus

You may notice that when you issue an `editor.update` or
`editor.dispatchCommand` then the editor can "steal focus" if there is
a selection and the editor is editable. This is because the Lexical
selection is reconciled to the DOM selection during reconciliation,
and the browser's focus follows its DOM selection.

If you want to make updates or dispatch commands to the editor without
changing the selection, can use the `SKIP_DOM_SELECTION_TAG` update tag
(added in v0.22.0):

```js
// Call this from an editor.update or command listener
$addUpdateTag(SKIP_DOM_SELECTION_TAG);
```

If you want to add this tag during processing of a `dispatchCommand`,
you can wrap it in an `editor.update`:

```js
// NOTE: If you are already in a command listener or editor.update,
//       do *not* nest a second editor.update! Nested updates have
//       confusing semantics (dispatchCommand will re-use the
//       current update without nesting)
editor.update(() => {
  $addUpdateTag(SKIP_DOM_SELECTION_TAG);
  editor.dispatchCommand(/* … */);
});
```

`SKIP_DOM_SELECTION_TAG` does not apply to the initial editor state setup
(the `$initialEditorState` property of the root extension, or
`initialConfig.editorState` with the legacy `LexicalComposer`). On first mount the editor still scrolls
to and focuses the initial selection. To prevent that, call
`$setSelection(null)` inside your initial state setup function:

```js
const editor = buildEditorFromExtensions({
  name: '[root]',
  // ...
  $initialEditorState: () => {
    // ... build your initial nodes ...
    $setSelection(null);
  },
});
```

If you have to support older versions of Lexical, you can mark the editor
as not editable during the update or dispatch.

```js
// NOTE: This code should be *outside* of your update or command listener, e.g.
//       directly in the DOM event listener
const prevEditable = editor.isEditable();
editor.setEditable(false);
editor.update(
  () => {
    // run your update code or editor.dispatchCommand in here
  }, {
    onUpdate: () => {
      editor.setEditable(prevEditable);
    },
  },
);
```

## Reading selection across a shadow boundary

If your plugin reads the DOM selection directly — through
`Selection.anchorNode`, `Selection.getRangeAt(0)`, or similar — and the
editor's `contentEditable` lives inside a `ShadowRoot`, the browser retargets
those reads to the shadow host. See
[Shadow DOM and iframes](./shadow-dom.md) for the shadow-aware helpers
(`getDOMSelectionPoints`, `getDOMSelectionRange`, etc.) that return the
un-retargeted boundary points; they fall through to the standard reads in the
plain light DOM, so there's nothing to do until you actually mount the editor
in a shadow tree.

### Selection change timing

`SELECTION_CHANGE_COMMAND` runs **before reconciliation**, in a writable update,
for native, programmatic, and non-range selection changes. `$getSelection()`
returns the pending selection; `$getPreviousSelection()` returns the selection
from the last committed state. A listener can normalize the selection or edit
nodes before that same update commits. Listeners that change the selection may
cause another notification before commit; they must converge.

This covers programmatic changes such as `node.select()`, which notify without
waiting for a DOM `selectionchange` event, and non-range selections such as
`NodeSelection` and `TableSelection`, as well as native range selections.

Selection listeners use the update's normal error handling. A throwing listener
reports through `onError` and can abort the entire pending update, including the
edit that triggered the notification and any other edits batched into that
commit, just like an explicit command dispatch inside that update. Because a
programmatic selection change also notifies, a listener error can abort content
edits made in the same update. Listener edits do not have a separate rollback
scope.

The DOM is not guaranteed to match the pending state, even for a `NodeSelection`.
New nodes may not have an element yet, and DOM ranges, layout, and focus may
still reflect the previous state. Schedule DOM-dependent work after the update
and read the reconciled state there:

```ts
editor.registerCommand(
  SELECTION_CHANGE_COMMAND,
  () => {
    $onUpdate(() => {
      editor.read('latest', () => {
        // Read $getSelection(), look up elements, or position floating UI here.
      });
    });
    return false;
  },
  COMMAND_PRIORITY_LOW,
);
```

Import `$onUpdate` and `COMMAND_PRIORITY_LOW` from `lexical`. Read selection and
nodes inside the callback rather than capturing mutable pending objects.
`registerUpdateListener` is another option for UI that must respond to every
commit, including content changes that do not change the selection. Updates
using `SKIP_DOM_SELECTION_TAG` still deliberately leave the browser selection
unsynchronized.

#### Update tags and local edits

The notification belongs to the pending update and inherits its tags. A listener
can inspect them with `$hasUpdateTag`. In particular, Yjs does not sync content
edits back to peers when the update is tagged `COLLABORATION_TAG` or
`HISTORIC_TAG`; history also treats `HISTORIC_TAG` as replaying an existing entry.
This applies to content edits made by selection listeners in those updates.

Listeners that only apply to local editing should skip those tagged updates. If
a remote change or undo must trigger an independent local content edit, schedule
it after the tagged commit with `$onUpdate`, then start a fresh `editor.update`:

```ts
const $applyLocalEdit = () => {
  // Read the current selection and nodes, then apply a convergent local edit.
};

editor.registerCommand(
  SELECTION_CHANGE_COMMAND,
  () => {
    if ($hasUpdateTag(COLLABORATION_TAG) || $hasUpdateTag(HISTORIC_TAG)) {
      $onUpdate(() => editor.update($applyLocalEdit));
    } else {
      $applyLocalEdit();
    }
    return false;
  },
  COMMAND_PRIORITY_LOW,
);
```

Import the tags and `$hasUpdateTag` from `lexical`. Re-read the state in the fresh
update; it has its own collaboration and undo behavior. Calling `editor.update`
directly inside the command listener queues a nested update that still belongs
to the same tagged commit.

Automatic range and cleared-selection notifications require a connected editor
root. Changes committed while those notifications are skipped advance the
selection baseline and are not replayed when the root reconnects. Non-range
selections continue to notify in unmounted and headless editors,
with the same pre-reconciliation timing. Native dirty-selection notifications
may fire even when the selection compares equal to the previous selection.

---

### Packages Lexical Website Docs Concepts Shadow Dom (`packages/lexical-website/docs/concepts/shadow-dom.md`)

---
sidebar_label: Shadow DOM and iframes
---

# Shadow DOM and iframes

A Lexical editor works when its `contentEditable` lives inside an **open**
[DOM `ShadowRoot`](https://developer.mozilla.org/docs/Web/API/ShadowRoot) (for
example inside a [web component](https://developer.mozilla.org/docs/Web/API/Web_components))
or inside an `<iframe>` document. Both are supported out of the box — you only
have to point the editor at the right root element.

## Embedding in a shadow root

Attach an open shadow root, render a `contentEditable` inside it, and pass that
element to `editor.setRootElement` (or, with `@lexical/react`, portal the
`ContentEditable` into the shadow root — React context flows across the portal,
so the editor is built exactly as it would be in the light DOM):

```js
const host = document.querySelector('#editor-host');
const shadow = host.attachShadow({mode: 'open'});
const contentEditable = document.createElement('div');
contentEditable.contentEditable = 'true';
shadow.appendChild(contentEditable);

editor.setRootElement(contentEditable);
```

Lexical detects the enclosing shadow root from the root element and resolves
selection through it. There is nothing else to configure.

### Why it needs platform support

Inside a shadow tree the browser **retargets** the document's selection to the
shadow host: `Selection.anchorNode`/`focusNode` and `Selection.getRangeAt`
report the host element, not the node the caret is actually in, and
`document.activeElement` reports the host rather than the focused element.
Lexical works around this using only standard platform APIs:

- Selection is read with
  [`Selection.getComposedRanges`](https://developer.mozilla.org/docs/Web/API/Selection/getComposedRanges)
  and [`Selection.direction`](https://developer.mozilla.org/docs/Web/API/Selection/direction),
  which return the real (un‑retargeted) boundary points.
- Focus is read with
  [`ShadowRoot.activeElement`](https://developer.mozilla.org/docs/Web/API/DocumentOrShadowRoot/activeElement)
  via [`Node.getRootNode`](https://developer.mozilla.org/docs/Web/API/Node/getRootNode).
- Caret movement and word/line deletion use the native `Selection.modify`, and
  selection is written with `Selection.setBaseAndExtent`, both of which operate
  on shadow‑tree nodes directly.
- Drop targets and image-drag hit‑tests resolve through
  [`Document.caretPositionFromPoint`](https://developer.mozilla.org/docs/Web/API/Document/caretPositionFromPoint)
  with the `shadowRoots` option so the dropped point lands on the real node
  rather than the retargeted host.

### Browser support

The shadow-aware path requires the platform APIs listed above. Lexical falls
back to the light‑DOM reads when those APIs are missing, so editors that don't
live in a shadow tree keep working on any engine the rest of Lexical supports.

| Platform API | Used for | Chrome / Edge | Firefox | Safari |
| --- | --- | --- | --- | --- |
| `Selection.getComposedRanges` | Reading the un‑retargeted boundary points | 137+ | 142+ | 17.0+ |
| `Selection.direction` | Mapping the composed range back onto anchor/focus | 137+ | 126+ | 17.0+ |
| `ShadowRoot.activeElement` | Resolving the focused element through the host | All modern | All modern | All modern |
| `Document.caretPositionFromPoint({shadowRoots})` | Shadow-aware drop / drag hit-tests | 128+ | 125+ | 18.1+ |

Lexical also supports the legacy variadic form of `getComposedRanges` shipped by
Safari 17–18.1, automatically choosing the dictionary or variadic call shape
at runtime.

### Closed shadow roots

A closed shadow root (`{mode: 'closed'}`) hides its contents from outside code:
`Node.getRootNode({composed: false})` returns the host on every external probe,
and `Selection.getComposedRanges({shadowRoots})` ignores the closed root. The
editor inside a closed shadow root cannot read its own selection through the
host, so closed shadow roots are **not supported**.

If you need style/markup encapsulation without selection isolation, mount the
editor in an open shadow root with `delegatesFocus: true` instead — outside
code can still observe focus and selection while CSS and markup remain
encapsulated.

### Styling

Shadow trees do not inherit the document's stylesheets, so your editor/theme
CSS has to live inside the shadow root. A `<style>` element appended to the
root is the simplest option; a
[constructable stylesheet](https://developer.mozilla.org/docs/Web/API/CSSStyleSheet#constructor)
adopted via `shadowRoot.adoptedStyleSheets` scales better when one stylesheet
serves several editor instances. The playground's `ShadowDomWrapper` clones
the page's existing `<link>` / `<style>` nodes into the shadow root so it can
reuse Vite's HMR-managed stylesheets. This is a property of Shadow DOM, not
of Lexical.

CSS custom properties and inherited HTML attributes (`dir`, `lang`) cross
the shadow boundary on their own, so a page-level rule like `:root {
--editor-bg: #1c1d22; }` or a host attribute like `<my-editor dir="rtl"
lang="ko">` propagates into the editor without further setup. User
preference media queries (`@media (prefers-color-scheme: dark)`,
`(prefers-reduced-motion: reduce)`, `(forced-colors: active)`) evaluate
inside the shadow root just like outside; place them in the shadow-mounted
stylesheet rather than the page sheet.

## Embedding in an iframe

An editor whose root element belongs to an `<iframe>` document is also
supported. Lexical reads the editor's `window`/`document` from the root
element (`rootElement.ownerDocument.defaultView`), so selection and focus are
resolved against the iframe rather than the top‑level document:

```js
const iframeDoc = iframe.contentDocument;
const contentEditable = iframeDoc.querySelector('#editor');

// The editor can be built in the parent frame; once its root element is set it uses
// the iframe's own window and document for selection and focus.
editor.setRootElement(contentEditable);
```

Selection inside an iframe is **not** retargeted (an iframe is a separate
document, not a shadow boundary), so `getComposedRanges` is not involved here —
the iframe's own selection already reports the correct nodes.

## Shadow-aware helpers

If a plugin reads the DOM selection or the focused element directly, use the
shadow/iframe‑aware helpers exported from `lexical` instead of
`Selection.anchorNode` / `document.activeElement`, so it keeps working in these
contexts. In the plain light DOM each helper degrades to the standard read
without any extra cost.

| Instead of | Use |
| --- | --- |
| `Selection.anchorNode` / `anchorOffset` / `focusNode` / `focusOffset` | `getDOMSelectionPoints(selection, rootElement)` |
| `selection.getRangeAt(0)` | `getDOMSelectionRange(selection, rootElement)` |
| Both shapes from one read | `getDOMSelectionRangeAndPoints(selection, rootElement)` |
| `selection.getComposedRanges` raw call | `getComposedStaticRange(selection, rootElement)` |
| `document.activeElement === el` | `getActiveElement(el) === el` |
| Deepest focused element through nested shadow trees | `getActiveElementDeep(document)` |
| `event.target` on a `composed: true` event listening above the shadow boundary | `getComposedEventTarget(event)` |
| `node.getRootNode() instanceof ShadowRoot` (cross‑realm‑safe) | `isDOMShadowRoot(node)` |
| Walking up to discover enclosing shadow roots | `getDOMShadowRoots(node)` |

All nine helpers are marked `@experimental` while the surface stabilizes — the
return shape may change, but the behavior in the light DOM is stable.

### `getDOMSelectionPoints` read semantics

In the light DOM `getDOMSelectionPoints` returns the live `Selection` itself,
so each property read is deferred and `$updateDOMSelection` only pays for the
synchronous style/layout recalculation that a `Selection.anchorNode` /
`focusNode` read triggers when it actually needs the value. Inside a shadow
tree the return is a snapshot taken at call time, including
`Selection.direction` so callers can branch on it explicitly. Read the four
points immediately after the call rather than caching the returned reference,
or use `points === domSelection` to detect the alias path.

If a future engine ships `getComposedRanges` without `Selection.direction`
(no current configuration matches — every engine that ships the former
also ships the latter), the snapshot's `direction` is `undefined` and
anchor/focus default to the StaticRange's tree order; callers needing strict
backward fidelity should check `direction !== undefined` before relying on
the swap.

## Form association

A Lexical editor inside a [form-associated custom
element](https://developer.mozilla.org/docs/Web/API/Web_components#form-associated_custom_elements)
participates in a `<form>` like any built-in form control: submission carries
the editor's value, form reset clears it, and validation flows through. The
sketch:

```
/* Detailed source-code truncated for AI context efficiency. */
```

Without the dirty-leaf/element gate every caret move would wake the form's
`oninput` listeners, so the `input` event matches `HTMLInputElement`'s
contract only after that guard. The initial `setFormValue` is what keeps a
form submission before the user types from carrying an empty value, since
the update listener has not fired yet at that point. A full reference
implementation lives in
[`examples/shadow-dom-web-component`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom-web-component).

DOM moves (re-parenting the host into a different `<form>` or list) trigger
`disconnectedCallback` followed by `connectedCallback`, which rebuilds the
editor against a fresh contentEditable. Round-trip the user's content the
same way `<input>` and `<textarea>` round-trip their `value` attribute:
cache the serialized state in `disconnectedCallback` and replay it through
`parseEditorState` on the next mount. The reference dev example uses a
`pendingState` field for exactly this.

The same form-associated host can also implement the rest of the
[form-associated callbacks](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements#form-associated_callbacks):
`formStateRestoreCallback(state, reason)` re-hydrates the editor on bfcache
navigation (`reason: 'restore'`) or form autocomplete restore
(`reason: 'autocomplete'`); `formAssociatedCallback(form)` reacts to
programmatic form moves; `formResetCallback()` clears the editor when the
surrounding form is reset; and `formDisabledCallback(isDisabled)` flips the
editor's editable state when an ancestor `<fieldset disabled>` toggles. The
serialized JSON `internals.setFormValue` published earlier is what comes
back in the state-restore callback.

## Common pitfalls

Moving an existing editor into a shadow root exposes a handful of recurring
mismatches between the DOM APIs you'd reach for and what they report.

### Event retargeting

Listeners above the shadow boundary — `window`, `document`'s
`selectionchange`, popups portaled to `document.body` — see `event.target`
retargeted to the shadow host, not the actual clicked element. A
`rootElement.contains(event.target)` gate therefore always rejects clicks
that came from the editor. Resolve through the composed path:

```js
const target = getComposedEventTarget(event);
if (target instanceof Node && rootElement.contains(target)) {
  // click really did land inside the shadow-hosted editor
}
```

### Focus probes

`document.activeElement` reports the host when focus is inside an open
shadow tree, so `document.activeElement === rootElement` is always false.
`getActiveElement(rootElement)` reads `DocumentOrShadowRoot.activeElement`
through `Node.getRootNode` instead, and `getActiveElementDeep` keeps
descending into nested shadow roots when an editor whose decorator embeds a
web component needs the innermost focused element.

```js
if (getActiveElement(rootElement) === rootElement) { ... }
```

### Outside-click handlers

A typical dropdown registers `document.addEventListener('click', ...)` and
calls `setShowDropDown(false)` whenever `button.contains(event.target)` is
false. From inside a shadow tree that check always fails (the target is the
host), so the dropdown closes on the very click that opened it. Compare
against `getComposedEventTarget(event)` instead — the same fix the playground's
`DropDown` and `@lexical/react`'s `DraggableBlockPlugin` use.

### Drop hit-tests

`document.caretRangeFromPoint` and the no-argument
`document.caretPositionFromPoint` return the host when the pointer is over
shadow content, so an image drop lands on the host rather than the textnode
under the cursor. `@lexical/clipboard`'s internal `caretFromPoint` helper switches to
`caretPositionFromPoint(x, y, {shadowRoots})` when `rootElement` lives in a
shadow tree, and verifies the returned offset node really did land inside
one of the requested shadow roots — engines that silently ignore the option
fall through to the legacy paths.

### Style mirror cleanup

If your shadow mount mirrors `<style>` / `<link>` nodes from `document.head`
and watches for additions via `MutationObserver`, mirror removals too;
otherwise stylesheets removed by HMR or a runtime theme swap linger inside
the shadow. `ShadowDomWrapper` tracks an `original → clone` map so an
upstream removal drops the corresponding clone, and on unmount it removes
every clone so React 18 StrictMode's double-mount can't accumulate
duplicates through the persistent shadow root.

Dev-mode HMR has two paths that a `childList`-only observer misses.
Firefox replaces a `<style>`'s text content in place, so the observer
needs `characterData + subtree` to see the mutation; Chrome and WebKit
update CSS through the CSSOM (`style.sheet.replaceSync` / `insertRule`),
which the DOM observer never sees at all. `ShadowDomWrapper` also
subscribes to Vite's `vite:afterUpdate` hook for an explicit resync of
every clone after each HMR pass — `import.meta.hot` is `undefined` in
production builds, so the hook is a no-op outside dev.

### Popover and dialog layout

The UA stylesheet defaults a closed
[Popover](https://developer.mozilla.org/docs/Web/API/Popover_API) (or a
`<dialog>` without `open`) to `display: none`. A base rule like
`#my-popover { display: flex; ... }` overrides that and leaves the popover
visible after `hidePopover()`. Scope layout to the open state instead —
`#my-popover:popover-open { display: flex; }` — so the closed state honors
the UA default. This is a popover-API gotcha, not a shadow-specific one,
but it surfaces here because floating UI anchored to a shadow-root
selection (a format popover or link editor that reads coordinates through
`getDOMSelectionRangeAndPoints`) is a common shadow integration pattern.

### Observers across the shadow boundary

`ResizeObserver`, `IntersectionObserver`, and `MutationObserver` observe
nodes inside an open shadow root with no special configuration — pass the
inner node (the contentEditable or the host) just like any other DOM
target. A `MutationObserver` registered on the host does not see mutations
inside the shadow tree, though; observe the contentEditable or a
shadow-internal container instead. The host's `attributeChangedCallback`
covers the host's own attribute changes.

## Migrating an existing light-DOM editor

The same code shape keeps working in a shadow root; migration is mostly
about removing pre-shadow workarounds. Direct selection reads
(`Selection.anchorNode`, `Selection.getRangeAt(0)`,
`document.querySelector('[contenteditable]')` used as an editor probe) route
through `getDOMSelectionPoints` / `getDOMSelectionRange` /
`getActiveElement`, and any `event.target` read above the shadow boundary
becomes `getComposedEventTarget(event)`. CSS variables inherit through the
shadow boundary but class declarations don't, so the editor/theme CSS has
to be adopted or cloned into the shadow root.

The focus and popup paths are the ones that surprise people in review:
outside-click handlers, blur-then-refocus, and "did the editor lose focus?"
probes usually need `getActiveElement(rootElement)` rather than
`document.activeElement`; floating UI that portals into `document.body` and
resolves hover with `element.getRootNode().elementFromPoint(x, y)` keeps the
same call shape but has to guard the popup root narrowing against a
detached popup whose `getRootNode()` returns itself.

## Examples

Runnable examples live in the repository:

- [`examples/shadow-dom`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom)
  — a React editor inside a shadow root with a light‑DOM toolbar.
- [`examples/shadow-dom-web-component`](https://github.com/facebook/lexical/tree/main/examples/shadow-dom-web-component)
  — a framework‑free `<lexical-editor>` custom element, form‑associated via
  `ElementInternals`. The page also mounts one instance inside a wrapper
  `<div>` that opens its own shadow root, exercising the multi‑level
  walk through two nested shadow boundaries.
- [`examples/vanilla-js-iframe`](https://github.com/facebook/lexical/tree/main/examples/vanilla-js-iframe)
  — an editor rendered into an `<iframe>`.
- The playground's **Render in Shadow DOM** setting toggles the same editor
  between light and shadow mounts, useful for spot-checking your plugins
  against both modes.

---

### Packages Lexical Website Docs Concepts Transforms (`packages/lexical-website/docs/concepts/transforms.md`)

# Node Transforms

Transforms are the most efficient mechanism to respond to changes to the EditorState.

For example:
User types a character and you want to color the word blue if the word is now equal to "congrats".
We programmatically add an `@Mention` to the editor, the `@Mention` is immediately next to another `@Mention` (`@Mention@Mention`). Since we believe this makes mentions hard to read, we want to destroy/replace both mentions and render them as plain TextNode's instead.

```js
const removeTransform = editor.registerNodeTransform(TextNode, (textNode) => {
  if (textNode.getTextContent() === 'blue') {
    textNode.setTextContent('green');
  }
});
```

## Syntax

```typescript
editor.registerNodeTransform<T extends LexicalNode>(klass: Klass<T>, listener: Transform<T>): () => void
```

## Lifecycle

Transforms are executed sequentially before changes are propagated to the DOM and multiple transforms still lead to a single DOM reconciliation (the most expensive operation in Lexical's lifecycle).

```mermaid
flowchart LR
  accTitle: Transforms lifecycle
  accDescr: A keyboard event leads to an update function. Its changes mark nodes dirty, and the transforms registered for those nodes run one after another. If a transform changed more nodes, the transforms for those nodes run again. When nothing is left dirty, the DOM is reconciled once and the update listeners run.
  event{{"Keyboard event"}} --> update["Update function"]
  update --> t1["transform1"]
  t1 --> t2["transform2"]
  t2 --> dirty{"Nodes still dirty?"}
  dirty -->|"yes"| t1
  dirty -->|"no"| reconcile["DOM reconciled once"]
  reconcile --> listeners["Update listeners"]
```

:::caution Beware!

While it is possible to achieve the same or very similar result through an [update listener](listeners.md#registerupdatelistener) followed by an update, this is highly discouraged as it triggers an additional render (the most expensive lifecycle operation).

Additionally, each cycle creates a brand new `EditorState` object which can interfere with features like undo/redo (`HistoryExtension`) if not handled correctly.

```js
editor.registerUpdateListener(() => {
  editor.update(() => {
    // Don't do this
  });
});
```

:::

### Dirty Nodes

*Dirty Leaves* (any LexicalNode that is not an ElementNode) and *Dirty Elements*
are tracked separately to support the transform heuristic.

Internally, there are two states for dirty nodes:
* Intentionally Dirty nodes (leaves or elements) had `node.getWritable()` or
  `node.markDirty()` (an alias) called on them. Maintaining the
  tree-of-doubly-linked-lists structure of a lexical document requires that
  this Intentionally Dirty state will propagate to immediate siblings and in
  some cases the parent node.
* Unintentionally Dirty Element nodes are an ancestor of a
  dirty node that were not explicitly marked as Intentionally Dirty
  Only elements can be Unintentionally Dirty, because
  leaves by definition can not have children.

The reconciler works by starting at the RootNode (which is the ancestor
of any attached node, and thus always dirty whenever any attached node
is dirty). Intentionally Dirty nodes have their createDOM and/or updateDOM
called. Dirty Elements reconcile all of their children. Thus, reconciliation
stops at the highest node in a subtree that has no dirty nodes (unless it
is running with a flag to do a full reconciliation which considers all
nodes as Intentionally Dirty).

:::info

The transform heuristic depends on these internal implementation details to
find a fixed point where no more transforms are required.

:::

### Transform heuristic

1. We transform leaves first. If transforms generate additional dirty nodes we repeat `step 1`. The reasoning behind this is that marking a leaf as dirty marks all its parent elements as dirty too.
2. We transform elements.
    - If element transforms generate additional dirty nodes we repeat `step 1`.
    - If element transforms only generate additional dirty elements we only repeat `step 2`.

A node will be marked as dirty on any (or most) modifications done to it, its children, or siblings in certain cases.

## Preconditions

Preconditions are fundamental for transforms to prevent them from running multiple times and ultimately causing an infinite loop.

Transforms are designed to run when nodes have been modified (aka Intentionally Dirty). For the most part, transforms only need to run once after the update but the sequential nature of transforms makes it possible to have order bias. Hence, transforms are run over and over until this particular type of Node is no longer marked as intentionally dirty by any of the transforms.

Hence, we have to make sure that the transforms do not mark the node dirty unnecessarily.

```js
// When a TextNode changes (marked as dirty) make it bold
editor.registerNodeTransform(TextNode, textNode => {
  // Important: Check current format state
  if (!textNode.hasFormat('bold')) {
    textNode.toggleFormat('bold');
  }
});
```

But oftentimes, the order is not important. The below would always end up in the result of the two transforms:

```js
// Extension 1
editor.registerNodeTransform(TextNode, textNode => {
  // This transform runs twice but does nothing the first time because it doesn't meet the preconditions
  if (textNode.getTextContent() === 'modified') {
    textNode.setTextContent('re-modified');
  }
})
// Extension 2
editor.registerNodeTransform(TextNode, textNode => {
  // This transform runs only once
  if (textNode.getTextContent() === 'original') {
    textNode.setTextContent('modified');
  }
})
// App
editor.registerUpdateListener(({editorState}) => {
  const text = editorState.read(() => $getRoot().getTextContent());
  // text === 're-modified'
});
```

:::info

The transform heuristic considers `RootNode` to be Intentionally Dirty whenever
any node is dirty, and it ensures that this transform is applied last after all
other transforms. In this way, you can consider it a "pre-update" listener,
which occurs before any DOM reconciliation has happened. As with any other
transform, it may get called multiple times in a given update (especially if
your RootNode transform makes any node dirty).

:::

## Transforms on parent nodes

Transforms are very specific to a type of node. This applies to both the declaration (`registerNodeTransform(ImageNode)`) and the times it triggers during an update cycle.

```js
// Won't trigger
editor.registerNodeTransform(ParagraphNode, () => {});
// Will trigger as TextNode was marked dirty
editor.registerNodeTransform(TextNode, () => {});
editor.update(() => {
  const textNode = $getNodeByKey('3');
  textNode.setTextContent('foo');
});
```

While the marked dirty rule is always true, there are some cases when it's not immediately obvious and/or we force nearby nodes to become dirty for the sake of easier transform logic:
You add a node to an ElementNode, the ElementNode and the newly added children are marked dirty, also its new immediate siblings
You remove a node, its parent is marked dirty, also the node's immediate siblings prior to being removed
You move a node via `replace`, rules 2 and 1 are applied.

```js
editor.registerNodeTransform(ParagraphNode, paragraph => {
 // Triggers
});
editor.update(() => {
  const paragraph = $getRoot().getFirstChild();
  paragraph.append($createTextNode('foo'));
});
```

## registerLexicalTextEntity

It is common to have certain nodes that are created/destroyed based on their text content and siblings. For example, `#lexical` is a valid hashtag whereas `#!lexical` is not.

This is a perfectly valid case for transforms but we have gone ahead and already built a utility transform wrapper for you for this specific case:

```typescript
registerLexicalTextEntity<T extends TextNode>(
  editor: LexicalEditor,
  getMatch: (text: string) => null | EntityMatch,
  targetNode: Klass<T>,
  createNode: (textNode: TextNode) => T,
): Array<() => void>;
```

## Examples

1. [Emojis](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/EmojisExtension/index.ts)
2. [AutoLink](https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/plugins/AutoLinkExtension/index.ts)
3. [HashtagExtension](https://github.com/facebook/lexical/blob/main/packages/lexical-hashtag/src/LexicalHashtagExtension.ts)

---

### Packages Lexical Website Docs Concepts Traversals (`packages/lexical-website/docs/concepts/traversals.md`)

# Node Traversals with NodeCaret

NodeCaret offers a unified and efficient way for traversing the document
tree, making it much easier to correctly implement traversals and avoid
edge cases around empty nodes and collapsed selections.

These new low-level functions were all designed to work together as a
fully featured relatively lightweight API to use in the core to
allow us to gradually address some edge cases and then simplify and shrink
the code. We expect higher-level utilities to be developed and shipped
in @lexical/utils or another module at a later date. The current overhead
should be less than 3kB in a production environment.

The NodeCaret API was introduced in lexical v0.25.0.

## Concepts

The core concept with `NodeCaret` is that you can represent any specific
point in the document by using an `origin` node, a `direction` that
points towards an adjacent node (`next` or `previous`), and a `type`
to specify whether the arrow points towards a sibling (`sibling`) or
towards a child (`child`).

All of these types have a `D` type parameter that must be a `CaretDirection`, so you
can not accidentally mix up `next` and `previous` carets. Many of them
also have a `T` type parameter that encodes the type of the `origin` node.

:::tip

The methods of a caret are designed to operate on nodes attached to the `origin`
in the designated direction, not the `origin` itself. For example, this code is
a no-op because it will attach a node to the `origin`, and then remove the node
that was just attached.

```ts
// The origin is unaffected (other than being marked dirty)
caret.insert($createTextNode('no-op')).remove();
```

:::

:::warning

Carets are immutable, and designed for low-level usage. There is no attempt
for carets to automatically update based on changes to the document
(this is a common source of bugs when working with `RangeSelection`).
Functions and methods that work with carets and are expected to change the
structure of the document will always return a possibly new caret.

The `origin` of a caret is the exact version of the object that it was
constructed with, all accessor methods on that origin will generally call
`origin.getLatest()` so the operations will see the latest version.

:::

### NodeCaret

`NodeCaret` is any `SiblingCaret` or any `ChildCaret`
* Typically constructed with `$getChildCaretOrSelf($getSiblingCaret(origin, direction))`
  which returns a `ChildCaret` when the origin is an `ElementNode`

:::tip

This type does not include `TextPointCaret` or `TextPointCaretSlice`,
so you will not have to consider those edge cases when you see this
more specific type.

:::

### SiblingCaret

`SiblingCaret` is a caret that points towards a sibling of the origin

* Constructed with `$getSiblingCaret(origin: LexicalNode, direction: CaretDirection)`
* The `next` direction points towards the right
* The `previous` direction points towards the left

|                        | → direction: `'next'`      | ← direction: `'previous'`     |
|------------------------|---------------------------|-------------------------------|
| `getParentAtCaret()`   | `origin.getParent()`      | `origin.getParent()`          |
| `getNodeAtCaret()`     | `origin.getNextSibling()` | `origin.getPreviousSibling()` |
| `insert(node)`         | `origin.insertAfter(node)`| `origin.insertBefore(node)`   |

### ChildCaret

`ChildCaret` is a caret that points towards the first or last child of the origin

* Constructed with `$getChildCaret(origin: ElementNode, direction: CaretDirection)`
* The `next` direction points towards the first child
* The `previous` direction points towards the last child

|                        | ↘ direction: `'next'`      | ↙ direction: `'previous'`     |
|------------------------|----------------------------|-------------------------------|
| `getParentAtCaret()`   | `origin`                   | `origin`                      |
| `getNodeAtCaret()`     | `origin.getFirstChild()`   | `origin.getLastChild()`       |
| `insert(node)`         | `origin.splice(0, 0, node)`| `origin.append(node)`         |

### PointCaret

`PointCaret` is any `TextPointCaret`, `SiblingCaret` or `ChildCaret`. This
type can be used to represent any point in the document that `PointType` can represent.

### TextPointCaret

`TextPointCaret` is basically a `SiblingCaret` with a `TextNode` origin and an `offset` property
* Constructed with `$getTextPointCaret(origin, direction, offset)`
* The `offset` property is an absolute index into the string
* The `next` direction implies all text content after `offset`
* The `previous` direction implies all text content before `offset`
* All methods that are also present on `SiblingCaret` behave in the same way

### TextPointCaretSlice

`TextPointCaretSlice` is a wrapper for `TextPointCaret` that provides a signed `distance`.

* Constructed with `$getTextPointCaretSlice(caret, distance)`
* `$splitTextPointCaretSlice(slice, selection?)` isolates the slice in a text node,
  splitting at its boundaries when needed. It returns `null` for an empty slice
  and the original node for a whole-node slice. Pass the range selection that
  produced the slice to preserve its endpoints, including element points and
  detached selections. Like `TextNode.splitText`, it does not treat token or
  segmented nodes as atomic.
* There are convenience methods like `removeTextSlice()` and `getTextContent()`,
  so it's not generally necessary to know the implementation details here
* `Math.min(caret.offset, caret.offset + distance)` refers to the start offset of the slice
* `Math.max(caret.offset, caret.offset + distance)` refers to the end offset of the slice
* The `direction` of the caret is generally ignored when working with a
  `TextPointCaretSlice`, the slice is in absolute string coordinates

:::info

The property name `distance` was chosen because `length` and `size` are
commonly used on other data structures in JavaScript and Lexical, and they
are overwhelmingly non-negative. While most uses of `distance` are also
non-negative, in some contexts such as computer graphics it is not uncommon
to use
[Signed distance functions](https://en.wikipedia.org/wiki/Signed_distance_function)
where the distance metric is signed.

In SDF terms, the subset of the space is `[offset, ∞)`. Any coordinate less
than the `offset` boundary is a negative distance; otherwise the distance is
non-negative.

:::

### CaretRange

`CaretRange` contains a pair of `PointCaret` that are in the same direction. It
is equivalent in purpose to a `RangeSelection`, and is what you would generally
use for depth first traversals.

* Constructed with `$getCaretRange(anchor, focus)`, `$caretRangeFromSelection(selection)`,
  or `$extendCaretToRange(anchor)`
* The `anchor` is the start of the range, generally where the selection originated,
  and it is "anchored" in place because when a selection grows or shrinks only the
  `focus` will be moved
* The `focus` is the end of the range, where the blinking cursor is, it's the current
  focus of the user
* Anchor and focus must point in the same direction. The `anchor` points towards the first
  node *in the range* and the focus points towards the first node *not in the range*
* The `getTextSlices()` method is essential to handle the literal edge cases where
  the anchor and/or focus are a `TextPointCaret`. These edges are *not* included
  in the default caret iteration of the `CaretRange`.

:::warning

If you are iterating a `CaretRange` you must consider the `getTextSlices()`
separately, they are not included in the iteration. This is so you don't have
to consider `TextPointCaretSlice` at every step. They are literal edge cases
that can only be at the start and/or end and typically have special
treatment (splitting instead of removing, for example).

:::

## Traversal Strategies



### Adjacent Caret Traversals

The lowest level building block for traversals with NodeCaret is the adjacent caret
traversal, which is supported directly by methods of NodeCaret.

`getAdjacentCaret()` - Gets a `SiblingCaret` for the node attached to
  `origin` in direction. If there is no attached node, it will return `null`

`getParentCaret(rootMode)` - Gets a `SiblingCaret` for the parent node
  of `origin` in the same direction. If there is no parent node, or the parent
  is a root according to `rootMode`, then it will return `null`. `rootMode`
  may be `'root'` to only return `null` for `RootNode` or `'shadowRoot'` to
  return `null` for `RootNode` or any `ElementNode` parent where
  `isShadowRoot()` returns true

`getChildCaret()` - Gets a `ChildCaret` for this origin, or `null` if the
  origin is not an `ElementNode`. Will return `this` if the caret is already
  a `ChildCaret`

For example, iterating all siblings:

```ts
// Note that NodeCaret<D> already implements Iterable<NodeCaret<D>> in this
// way, so this function is not very useful. You can just use startCaret as
// the iterable.
function *$iterSiblings<D extends CaretDirection>(
  startCaret: NodeCaret<D>
): Iterable<SiblingCaret<LexicalNode, D>> {
  // Note that we start at the adjacent caret. The start caret
  // points away from the origin node, so we do not want to
  // trick ourselves into thinking that that origin is included.
  for (
    let caret = startCaret.getAdjacentCaret();
    caret !== null;
    caret = caret.getAdjacentCaret()
  ) {
    yield caret;
  }
}
```

### Examples

Given the following document tree, here are some examples of using the
adjacent node traversal:

Root
* Paragraph A
  * Text A1
  * Link A2
    * Text A3
  * Text A4
* Paragraph B
  * Text B1
* Paragraph C

```ts
// The root does not have sibling nodes
const carets = [...$getSiblingCaret($getRoot(), 'next')];
expect(carets).toEqual([]);
```

```ts
// The adjacent node to a ChildNode is its first or last child
// and is always a SiblingNode. It does not traverse deeper.
const carets = [...$getChildCaret($getRoot(), 'next')];

// next starts at the first child
expect(carets).toEqual([
  $getSiblingCaret(paragraphA, 'next'),
  $getSiblingCaret(paragraphB, 'next'),
  $getSiblingCaret(paragraphC, 'next'),
]);

// previous starts at the last child
const prevCarets = [...$getChildCaret($getRoot(), 'previous')];
expect(prevCarets).toEqual([
  $getSiblingCaret(paragraphC, 'previous'),
  $getSiblingCaret(paragraphB, 'previous'),
  $getSiblingCaret(paragraphA, 'previous'),
]);
```

```ts
// The iteration starts at the node where the head of the "arrow"
// is pointing, which is away from the origin (the tail of the "arrow").
const carets = [...$getSiblingCaret(paragraphB, 'next')];
expect(carets).toEqual([
  $getSiblingCaret(paragraphC, 'next'),
]);

const prevCarets = [...$getSiblingCaret(paragraphB, 'previous')];
expect(prevCarets).toEqual([
  $getSiblingCaret(paragraphA, 'previous'),
]);
```

### Depth First Caret Traversals

The strategy to do a depth-first caret traversal is to use an adjacent caret
traversal and immediately use a `ChildCaret` any time that an `ElementNode`
origin is encountered. This strategy yields all possible carets, but each
ElementNode in the traversal may be yielded once or twice (a `ChildCaret` on
enter, and a `SiblingCaret` on leave). Allowing you to see whether an
`ElementNode` is partially included in the range or not is one of the
reasons that this abstraction exists.

```ts
function *$iterCaretsDepthFirst<D extends CaretDirection>(
  startCaret: NodeCaret<D>
): Iterable<NodeCaret<D>> {
  function step(prevCaret: NodeCaret<D>): null | NodeCaret<D> {
    // Get the adjacent SiblingCaret
    const nextCaret = prevCaret.getAdjacentCaret();
    return (
      // If there is a sibling, try and get a ChildCaret from it
      (nextCaret && nextCaret.getChildCaret()) ||
      // Return the sibling if there is one
      nextCaret ||
      // Return a SiblingCaret of the parent, if there is one
      prevCaret.getParentCaret('root')
    );
  }
  // You may add an additional check here, usually some specific
  // caret to terminate the iteration with (such as the parent caret
  // of startCaret):
  //
  //  `caret !== null || caret.is(endCaret)`
  //
  for (
    let caret = step(startCaret);
    caret !== null;
    caret = step(caret)
  ) {
    yield caret;
  }
}
```

Normally this type of iteration would be done from a `CaretRange`, where you
would specify a precise end caret (focus).

```ts
function $iterCaretsDepthFirst<D extends CaretDirection>(
  startCaret: NodeCaret<D>,
  endCaret?: NodeCaret<D>,
): Iterable<NodeCaret<D>> {
  return $getCaretRange(
    startCaret,
    // Use the root as the default end caret, but you might choose
    // to use startCaret.getParentCaret('root') for example
    endCaret || $getSiblingCaret($getRoot(), startCaret.direction)
  );
}
```

To get all nodes that are entirely selected between two carets:

```ts
function *$iterNodesDepthFirst<D extends CaretDirection>(
  startCaret: NodeCaret<D>,
  endCaret: NodeCaret<D> = $getChildCaret($getRoot(), startCaret.direction),
): Iterable<LexicalNode> {
  const seen = new Set<NodeKey>();
  for (const caret of $getCaretRange(startCaret, endCaret)) {
    const {origin} = caret;
    if ($isChildCaret(caret)) {
      seen.add(origin.getKey());
    } else if (!$isElementNode(origin) || seen.has(origin.getKey())) {
      // If the origin is an element and we have not seen it as a ChildCaret
      // then it was not entirely in the CaretRange
      yield origin;
    }
  }
}
```

### Examples

Given the following document tree, here are some examples of using the
depth-first node traversal (with a `CaretRange`):

Root
* Paragraph A
  * Text A1
  * Link A2
    * Text A3
  * Text A4
* Paragraph B
  * Text B1
* Paragraph C

```ts
// A full traversal of the document from root
const carets = [...$getCaretRange(
  // Start with the arrow pointing towards the first child of root
  $getChildCaret($getRoot(), 'next'),
  // End when the arrow points away from root
  $getSiblingCaret($getRoot(), 'next'),
)];
expect(carets).toEqual([
  $getChildCaret(paragraphA, 'next'),   // enter Paragraph A
  $getSiblingCaret(textA1, 'next'),
  $getChildCaret(linkA2, 'next'),       // enter Link A2
  $getSiblingCaret(textA3, 'next'),
  $getSiblingCaret(linkA2, 'next'),     // leave Link A2
  $getSiblingCaret(textA4, 'next'),
  $getSiblingCaret(paragraphA, 'next'), // leave Paragraph A
  $getChildCaret(paragraphB, 'next'),   // enter Paragraph B
  $getSiblingCaret(textB1, 'next'),
  $getSiblingCaret(paragraphB, 'next'), // leave Paragraph B
  $getChildCaret(paragraphC, 'next'),   // enter Paragraph C
  $getSiblingCaret(paragraphC, 'next'), // leave Paragraph C
]);
```

## Future Direction

It's expected that higher-level abstractions will be built on top of this
outside of the core, either in @lexical/utils or a separate companion package.
This is just designed to be the lowest-level layer with a consistent and
type-safe interface. That sort of abstraction will probably look a little bit
like cheerio or jQuery, but for working with Lexical documents. It is not
expected that more abstractions will be added to the core.

In order to reduce code size and eliminate bugs, more of the core will be
refactored to use NodeCaret internally.

Once this happens, it's possible that the internal structure of PointType
and/or RangeSelection may change to accommodate NodeCaret, as it is more
resilient to document changes (only changes that directly affect the
origin node will "break" the point). A simple version of this would be to
create a caret any time that the point changes, and use that caret
as a fallback if the selection would otherwise be lost.

It may be the case that NodeCaret will become the lowest level API, working
directly with private LexicalNode/ElementNode internals. When/if that happens,
the methods on LexicalNode will remain for backwards compatibility,
but overriding them will not be supported. It isn't particularly safe to
override them as-is anyway, and these overrides are frequently the
root cause of bugs (e.g. parents that remove themselves after an operation
on a child, causing the point to be lost unless the caller was sophisticated
enough to store the array of parents).

## History

Before NodeCaret, Lexical's core API offered a relatively low-level DOM-like
interface for working with nodes and traversing them. It has accumulated
many functions over time for performing various kinds of traversals around
the tree (finding ancestors, children, depth, siblings, etc.), but most of
them are not implemented in a way that makes them easy to combine
efficiently, and many of them have edge cases that are difficult to avoid
and can't really be addressed without breaking compatibility.

Many of these functions also have a lot of edge cases, particularly around
assuming the reference nodes are inclusive. Many are also left-to-right
biased, don't offer an iterative version that can be aborted early or
consumed on the fly, etc.

Refactoring many of these to use something like `PointType` would almost
be sufficient for many of these use cases, but the representation of
that type is inefficient and error-prone as any mutation to the tree
requires that each point be manually recomputed. `PointType` is also
directionless, forcing a specific left-to-right bias into most APIs.
`RangeSelection` can be used in many cases because a direction can
be inferred from any two different points, but that collapses with
a single point. It's also impractical to use `RangeSelection`
concurrently with mutations due to the problems with `PointType`.

NodeCaret was born out of frustration with these APIs and a desire
to unify it all in a coherent way to simplify and reduce errors in
the core.

## Terminology

### Caret

The term Caret was chosen because it is concise and specific term
specific to a point in a text document. Lexical is "an extensible text editor
framework" so it makes sense that navigation in the document would use
terms relevant to text. Most other terms such as Cursor or Point
already have meanings in Lexical and/or are less specific.

See also:
- [Caret](https://developer.mozilla.org/en-US/docs/Glossary/Caret)
- [Caret navigation](https://en.wikipedia.org/wiki/Caret_navigation)

### Origin

The origin is the reference node for a NodeCaret. Absolute coordinates
are determined by combining this origin node and an "arrow" that points
towards to where the adjacent node is (or could be). The "arrow" is
determined by the `direction` and `type` of the caret.

In a way this "arrow" is considered to be something like a unit vector
to indicate the direction, and adding it to an origin allows you to specify
an absolute location relative to that origin. Unlike the other coordinate
systems available in Lexical, it does not need recomputing whenever
siblings or a parent changes, so long as the origin node is still attached.

See also:
- [Origin](https://en.wikipedia.org/wiki/Origin_(mathematics))
- [Unit Vector](https://en.wikipedia.org/wiki/Unit_vector)

### ChildCaret / SiblingCaret

These were chosen because they match the existing methods on `ElementNode`
and `LexicalNode` (`getFirstChild`, `getNextSibling`, etc.)

### Direction

`'next'` and `'previous'` were chosen for direction mostly to match the
existing methods such as `getNextSibling()` that exist in DOM and in Lexical.
Using other words such as `'left'` and `'right'` would be ambiguous since
text direction can be bidirectional and already uses the terms left-to-right
and right-to-left.

### Distance

The property name `distance` was chosen for `TextPointCaretSlice` because
`length` and `size` are commonly used on other data structures in JavaScript
and Lexical, and they are overwhelmingly non-negative. While most uses of
`distance` are also non-negative, in some contexts such as computer graphics
it is not uncommon to use
[Signed distance functions](https://en.wikipedia.org/wiki/Signed_distance_function)
where the distance metric is signed.

In SDF terms, the subset of the space is `[offset, ∞)`. Any coordinate less
than the `offset` boundary is a negative distance; otherwise the distance is
non-negative.

---

### Packages Lexical Website Docs Concepts Updates (`packages/lexical-website/docs/concepts/updates.mdx`)

import UpdateLifecycle from '@site/src/components/UpdateLifecycle';

# Updates

Every change to the document happens in an update. Updates are synchronous:
the callback you pass to `editor.update()` runs right away and changes a
pending copy of the editor state. Committing that state and applying it to the
DOM is batched, so several updates in the same tick lead to one DOM change.
See [Editor State](./editor-state.md#updating-state) for how to read and write
the state inside an update.

## The update lifecycle {#update-lifecycle}

Lexical uses double-buffering. The current editor state is frozen; an update
works on a pending copy, and updates made in the same tick are batched into
it. When the batch commits, the pending state becomes the new, frozen current
state, and the reconciler changes only the parts of the DOM that belong to
nodes the updates changed, so it skips most of the diffing a virtual DOM would
do. Keeping every committed state frozen is also what makes features such as
undo/redo cheap to implement. This is what happens, in order:

```
/* Detailed source-code truncated for AI context efficiency. */
```

To see how a real change moves through these phases, step through one of
these examples:

<UpdateLifecycle />

To make an update commit before `editor.update()` returns, pass
`{discrete: true}`; see
[Synchronous reconciliation with discrete updates](./editor-state.md#synchronous-reconciliation-with-discrete-updates).

## Changes from outside Lexical {#outside-changes}

The editor state, not the DOM, is the source of truth. For some plain typing,
Lexical lets the browser change the DOM itself for performance and then
updates the editor state from the `input` event. Beyond that, Lexical watches
its root element with a `MutationObserver` (paused while the reconciler makes
its own changes) and handles any other change that did not come from Lexical.
It keeps a change only if it looks like native text input, and reverts
everything else:

- **Kept: text changes inside a text node.** When the characters of a DOM
  text node that Lexical rendered change, which is what typing, spellcheck,
  autocorrect, and IME composition produce, Lexical reads the new text into
  the matching `TextNode`. Changes that arrive right after a text input event
  are left to the `input` handler instead.
- **Reverted: structural changes.** Elements or other DOM nodes added or
  removed inside the editor, for example by a browser extension, a script, or
  the browser's own editing of block structure, are undone. Added nodes are
  removed, removed nodes are put back by their parent node, stray `<br>`
  elements the browser adds are cleaned up, and the previous selection is
  restored, so the DOM matches the current editor state again.
- **Ignored: DOM that Lexical does not manage.** The contents of decorator
  nodes belong to your framework, and DOM that a node or extension adds on
  purpose can be marked with `setDOMUnmanaged()` so the observer leaves it
  alone.

## Update Tags

Update tags are string identifiers that can be attached to an update to indicate its type or purpose. They can be used to control how updates are processed, merged, or handled by listeners. Multiple tags can be used in a single update.

You can add tags in two ways:

1. Using the `tag` option in `editor.update()`:

```js
import {HISTORY_PUSH_TAG, PASTE_TAG} from 'lexical';

editor.update(
  () => {
    // Your update code
  },
  {
    tag: HISTORY_PUSH_TAG, // Single tag
  },
);

editor.update(
  () => {
    // Your update code
  },
  {
    tag: [HISTORY_PUSH_TAG, PASTE_TAG], // Multiple tags
  },
);
```

2. Using the `$addUpdateTag()` function within an update:

```js
import {HISTORY_PUSH_TAG} from 'lexical';

editor.update(() => {
  $addUpdateTag(HISTORY_PUSH_TAG);
  // Your update code
});
```

You can check if a tag is present using `$hasUpdateTag()`:

```js
import {HISTORIC_TAG} from 'lexical';

editor.update(() => {
  $addUpdateTag(HISTORIC_TAG);
  console.log($hasUpdateTag(HISTORIC_TAG)); // true
});
```

Note: While update tags can be checked within the same update using `$hasUpdateTag()`, they are typically accessed in update and mutation listeners through the `tags` and `updateTags` properties in their respective payloads. Here's the more common usage pattern:

```js
import {HISTORIC_TAG} from 'lexical';

editor.registerUpdateListener(({tags}) => {
  if (tags.has(HISTORIC_TAG)) {
    // Handle updates with historic tag
  }
});

editor.registerMutationListener(MyNode, (mutatedNodes, {updateTags}) => {
  // updateTags contains tags from the current update
  if (updateTags.has(HISTORIC_TAG)) {
    // Handle mutations with historic tag
  }
});
```

### Common Update Tags

Lexical provides several built-in update tags that are exported as constants:

- `HISTORIC_TAG`: Indicates that the update is related to history operations (undo/redo)
- `HISTORY_PUSH_TAG`: Forces a new history entry to be created
- `HISTORY_MERGE_TAG`: Merges the current update with the previous history entry
- `PASTE_TAG`: Indicates that the update is related to a paste operation. `@lexical/history` treats this as a history boundary so the paste gets its own undo entry
- `CUT_TAG`: Indicates that the update is related to a cut operation. `@lexical/history` treats this as a history boundary so the cut gets its own undo entry
- `COLLABORATION_TAG`: Indicates that the update is related to collaborative editing
- `SKIP_COLLAB_TAG`: Indicates that the update should skip collaborative sync
- `SKIP_SCROLL_INTO_VIEW_TAG`: Prevents scrolling the selection into view
- `SKIP_DOM_SELECTION_TAG`: Prevents updating the DOM selection (useful for updates that shouldn't affect focus)

### Tag Validation

To prevent typos and ensure type safety when using update tags, Lexical exports constants for all built-in tags. It's recommended to always use these constants instead of string literals:

```js
import {HISTORIC_TAG, HISTORY_PUSH_TAG, COLLABORATION_TAG} from 'lexical';

editor.update(() => {
  // Using constants ensures type safety and prevents typos
  $addUpdateTag(HISTORIC_TAG);

  // These constants can be used in update options
  editor.update(
    () => {
      // Your update code
    },
    {
      tag: HISTORY_PUSH_TAG,
    },
  );

  // And in listener checks
  editor.registerUpdateListener(({tags}) => {
    if (tags.has(COLLABORATION_TAG)) {
      // Handle collaborative updates
    }
  });
});
```

### Custom Tags

While Lexical provides common tags as constants, you can also define your own constants for custom tags to maintain consistency and type safety:

```js
// Define your custom tags as constants
const MY_FEATURE_TAG = 'my-custom-feature';
const MY_UPDATE_TAG = 'my-custom-update';

editor.update(
  () => {
    $addUpdateTag(MY_FEATURE_TAG);
  },
  {
    tag: MY_UPDATE_TAG,
  },
);

// Listen for updates with specific tags
editor.registerUpdateListener(({tags}) => {
  if (tags.has(MY_FEATURE_TAG)) {
    // Handle updates from your custom feature
  }
});
```

---

### Packages Lexical Website Docs Extensions Defining Extensions (`packages/lexical-website/docs/extensions/defining-extensions.md`)

# Defining Extensions

An extension is an plain JavaScript object that complies with the
[LexicalExtension](/docs/api/modules/lexical#lexicalextension) interface.
The [defineExtension](/docs/api/modules/lexical#defineextension) function
is merely an identity function to make TypeScript inference convenient.
It will be compiled down to the following, and likely optimized out by your
bundler or minifier:

```js
function defineExtension(extension) {
  return extension;
}
```

Extensions *must* be stable references. It is most useful to define them at
module scope, but if for some reason you have to define it inside of a
React component you'll need some way to make it stable such as with
`useState`, `useMemo`, or `useRef` as appropriate.

## Required Properties

### name

The only required property for an extension is `name`. The name is a string
that should be unique to that extension your editor. It is best practice to
use a scope for your project or organization to ensure that reusable
extensions don't conflict with each other.

The general practice in the lexical repository is to use the name of the
package as the extension name when it only makes sense to export a single
extension, (e.g. `DragonExtension` has the name `@lexical/dragon`), or
with an additional path if multiple extensions are exported (e.g.
`AutoFocusExtension` has the name `@lexical/extension/AutoFocus`).

## InitialEditorConfig

Extensions can specify configuration overrides for the editor, these are
defined by the
[InitialEditorConfig](/docs/api/modules/lexical#initialeditorconfig)
interface. See the API docs for the full list of properties. Every property
here has some default.

### Merged properties

For an extension designed to be used as a dependency, it can also be useful
to specify properties that are merged such as
`html` (override HTML import/export),
`nodes` (register new nodes or node overrides with the editor),
or `theme` (specify classes to be used for nodes provided by lexical).

```ts
export const CodeExtension = defineExtension({
  name: '@lexical/code',
  nodes: () => [CodeNode, CodeHighlightNode],
});
```

### Root properties

There are other properties that are more useful for the "root" extension
that you provide to
[buildEditorFromExtensions](/docs/api/modules/lexical_extension#buildeditorfromextensions)
or the `extension` prop of
[LexicalExtensionComposer](/docs/api/modules/lexical_react_LexicalExtensionComposer#lexicalextensioncomposer)
where you would specify properties that can only be meaningfully set
once such as `$initialEditorState`, `editable`, `onError`, `namespace` or `parentEditor`.

There is no requirement that these properties be set from any specific level
of the extension hierarchy, or at all, but it is only meaningful to set them
once per editor.

```ts
const editor = buildEditorFromExtensions(
  defineExtension({
    name: "@example/basic-rich-text-editor",
    namespace: "basic-rich-text-editor",
    dependencies: [RichTextExtension],
    register: (editor: LexicalEditor) => {
      console.log("Editor Created");
      return () => console.log("Editor Disposed");
    },
  }),
);
```

## Extension Dependencies

Lexical Extensions have two ways for extensions to depend on and specify
configuration for each other:

- [dependencies](#dependencies) are direct (by reference)
- [peerDependencies](#peerdependencies) are indirect (by name) and optional

The exception to this rule is with `peerDependencies`. These are optional
dependencies

### dependencies

The `dependencies` property is an array of extensions by reference that your
extension depends on. For example, if your extension uses React then it should
depend on the `ReactExtension`.

Dependencies form a directed acyclic graph. This means that there must not
be any circular dependencies. If extension A depends on extension B then
there must not be any dependency path from B to A
(or from A to A).

This array can contain either direct references to extensions, or calls to
[configExtension](/docs/api/modules/lexical#configextension).

```ts
export const ExampleExtension = defineExtension({
  name: "@example/extension",
  dependencies: [
    SomeExtension,
    configExtension(ReactExtension, {
      decorators: [<ExampleDecorator />]
    }),
  ],
});
```

### peerDependencies

The `peerDependencies` property is an array of optional extensions by name.
They are not requirements, but specifying them here allows your extension
to find them at runtime and override configuration for them if they are
built with the editor. You can use
[declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency) to
build this array with type inference.

Since these are optional and by name, an extension may declare a peer it does
not import. The graph must still be acyclic: peer edges take part in the same
topological sort as `dependencies`, so two extensions that peer-depend on each
other and are both present will fail to build.

This is rarely needed in practice and is considered an advanced use case,
typically to avoid a direct import or dependency loop. See
[Peer Dependencies](/docs/extensions/peer-dependencies) for more detail.

### conflictsWith

The `conflictsWith` property is an array of extensions by name that are
known to conflict with this extension. For example, it does not make
sense to have
[RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) and
[PlainTextExtension](/docs/api/modules/lexical_plain-text#plaintextextension)
in the same editor.

```ts
export const PlainTextExtension = defineExtension({
  conflictsWith: ['@lexical/rich-text'],
  dependencies: [
    DragonExtension,
    NormalizeInlineElementsExtension,
    NormalizeTripleClickSelectionExtension,
  ],
  name: '@lexical/plain-text',
  register: registerPlainText,
});
```

This is rarely needed in practice and is considered an advanced use case,
but it can provide helpful early errors when it is needed.

## Phases

The construction of an editor using extensions is done in sequential phases,
and the following properties are useful ways to hook into that process at
specific points.

### config

`config` is an object that is the default configuration for your extension,
properties of this object can be overridden by other extensions using
[configExtension](/docs/api/modules/lexical#configextension) or
[declarePeerDependency](/docs/api/modules/lexical#declarepeerdependency).

This object is used in later phases to build the init and/or output.

The config phase happens before the editor is constructed.

### mergeConfig

`mergeConfig` is a function that can be used to merge configuration
with its overrides if you need something more fine-grained than a
shallow object merge, such as concatenating arrays. For example:

```ts
interface StringArrayConfig {
  array: string[];
}
const StringArrayExtension = defineExtension({
  config: safeCast<StringArrayConfig>({array: []}),
  name: "@example/StringArray",
  mergeConfig(a, b) {
    const config = shallowMergeConfig(a, b);
    if (b.array) {
      config.array =
        b.array.length > 0
          ? [...a.array, ...b.array]
          : a.array;
    }
    return config;
  },
});
```

The default implementation is
[shallowMergeConfig](/docs/api/modules/lexical#shallowmergeconfig),
most extensions will not need to override this.

### init

The init phase happens before the editor is constructed, but after
the merged editor configuration is available and all extensions are
configured. It can be used to reference configuration
from peers, compute data that should be available during
[build](#build), and is generally a last resort for making mutations
to any extension or editor configuration before the editor is
created.

The result of this function is available in later phases.

This is rarely needed in practice and is considered an advanced use case.

### build

The build phase happens after the editor is constructed — it receives the
`LexicalEditor` — but before [register](#register), and after
[config](#config) and [init](#init). The return value of the `build`
phase is `output` and is available for later phases.

`output` is generally how extensions provide functionality to each
other and to the nodes in your application. A very common use case is
to build signals from configuration with `namedSignals` so that the
behavior of your extension can be modified at runtime (e.g. `disabled`
is a very common use case).

Other use cases for build's `output` are to provide
shared data structures, typed theme configuration, references to the
commands that the extension implements, etc.

```ts
export interface TabIndentationConfig {
  disabled: boolean;
  maxIndent: null | number;
  /**
   * By default, indents are set on all elements for which the
   * {@link ElementNode.canIndent} returns true. This option allows you to set
   * indents for specific nodes without overriding the method for others.
   */
  $canIndent: CanIndentPredicate;
}
export const TabIndentationExtension = defineExtension({
  build(editor, config, state) {
    return namedSignals(config);
  },
  config: safeCast<TabIndentationConfig>({
    $canIndent: $defaultCanIndent,
    disabled: false,
    maxIndent: null,
  }),
  name: '@lexical/extension/TabIndentation',
  register(editor, config, state) {
    const {disabled, maxIndent, $canIndent} = state.getOutput();
    return effect(() => {
      if (!disabled.value) {
        return registerTabIndentation(editor, maxIndent, $canIndent);
      }
    });
  },
});
```

### register

This happens after the editor has been constructed. This is where you will
register any commands, listeners, etc. that your extension needs. It can use
the result of `init` or `build` via `state.getInitResult()` and
`state.getOutput()` respectively.

The return value is a dispose function, typically the result of `mergeRegister`.

### afterRegistration

This happens after [register](#register) for every extension has been called,
so all commands should be registered. This is the phase when the
`$initialEditorState` is applied to the editor by `InitialStateExtension`,
so `editor.setRootElement` should be called no sooner than this phase (it may
of course be called after the editor is built, outside of extensions).

The return value is a dispose function, typically the result of
`mergeRegister`.

---

### Packages Lexical Website Docs Extensions Design (`packages/lexical-website/docs/extensions/design.md`)

# Design Doc

See also the initial PR [#7706](https://github.com/facebook/lexical/pull/7706)

## Guiding principles

### Minimize boilerplate

Wherever possible, sensible defaults should be provided and it must be
possible to override them. This is why a `Extension` has the `config` property
which specifies the initial configuration.

### Full backwards compatibility

Almost anything that you do with the editor or a legacy React plug-in should
just work. Upgrading to use a `Extension` should be a simple change.

### Minimize change required to add an Extension

Ideally, adding an extension should be two or three lines of code:

- Import it
- Add it as a dependency (with config if necessary)
- Add UI related code (only if needed, e.g. has to be rendered in a specific place in the DOM)

### Avoid invalid states

TypeScript goes a long way here, but it's only as good as the API design.
For example, splitting up the workflow for an Extension to have separate `Config`,
`Init` and `Output` types was something that evolved over time. In earlier
prototypes, `Config` was overloaded to maintain all of that state, but it
required a lot more runtime support and type assertions to check that
the state was properly initialized for that phase.

### Try to avoid undecipherable type errors

There's only so much that can be done to make readable error messages
out of complex type expressions, so the API is designed to avoid them
when possible. See also [microsoft/TypeScript#23689](https://github.com/microsoft/TypeScript/issues/23689).

## Inspiration

### [ESLint Flat Plugins](https://eslint.org/docs/latest/extend/plugin-migration-flat-config)

These have many of the same constraints as this project, although of course
are used for a very different purpose

### [MDXEditor](https://mdxeditor.dev/)

This editor is based on Lexical, but provides its own mechanism for plugins
based on a third party state management library. These plugins don't
really expose any type-safe configuration.

### [@payloadcms/richtext-lexical](https://www.npmjs.com/package/@payloadcms/richtext-lexical)

Another Lexical based editor framework for use inside the
[Payload](https://payloadcms.com/) CMS. It also doesn't really expose type-safe
configuration, but it does seem to have quite a lot of features and seems
well-designed for laziness and SSR. Features are conceptually similar to Extensions.

### [Effect](https://effect.website/)

This library is pretty much state of the art for type safety with a focus on
composition and usability.

### Tighter integration with LexicalEditor

This package was originally prototyped as an entirely optional feature,
as adoption increases it's expected that more of Lexical will directly
depend on it.

## Intentional trade-offs

### No compile-time support for dependency resolution

The current theory is that it would require too much TypeScript
in order to carry around the list of all dependency names that
exist in the graph, and would likely add another type argument
to `LexicalExtension`.

The features that this blocks are:

- Compile-time support for detecting Extension conflicts. Detecting that two
  extensions defined with the same name (but are not identical object
  references, because a dependency can be shared!) would also be quite the
  undertaking in TypeScript. This is automatically detected at runtime by
  the builder.
- Compile-time support for required configuration without defaults.
  An extension can implement this at runtime in `init` or `register`.
- Compile-time support for required peer dependencies. A use case for this
  would be the requirement of a `ReactProviderExtension` provided by either
  `LexicalExtensionComposer` or `ReactPluginHostExtension`. An extension can implement this
  at runtime in `init` or `register`.

Generally speaking, all of these are already surfaced as runtime errors
while building the editor with sufficient information to quickly track
down the root cause.

## Known Missing Features

### Direct support for devtools

This is a TODO, the infrastructure was designed with this in mind.

### Documented patterns for RSC/SSR/Headless

Having a known peer dependency that is used to declare SSR may help
guide future Extension development. In many cases there are decorator nodes
that have React dependencies that you do not want to (or can not) render
in an SSR context. For example, RSC can not be supported anywhere that
`React.Context` is used which is everywhere that React is currently used
in Lexical.

Another option would be to build separate entrypoints for the SSR use case,
which is more of an RSC specific strategy, but we could encourage people
to use those import conditions even in a Vanilla JS or some other bespoke
SSR use case. That strategy may make it hard to support including both
"headless" and "non-headless" in the same module.

---

### Packages Lexical Website Docs Extensions Faq (`packages/lexical-website/docs/extensions/faq.mdx`)

# FAQ

## What's the compatibility story?

All Legacy Plugins (React Components) are compatible with Lexical Extensions!

Lexical Extensions are _only_ compatible with editors created using the
Lexical Extension API (`buildEditorFromExtensions`, or an abstraction that uses
it internally such as `LexicalExtensionComposer`).

See the [Migration Guide](./migration) for more detailed examples!

## Why is it called an Extension instead of a Plugin?

Legacy reasons. Historically, Lexical has used the term Plugin to refer
to a React component that adds behavior to the editor. Transitioning
this word to another meaning would be confusing.

It would also be a tricky transition for public APIs, since the ideal
names for exports are already taken, so the legacy Plugin and the new
Extension could not be colocated if they were both called Plugin!

## What about lazy loading?

Lazy loading does not need any special support! All of the functions
that you implement with extensions must be synchronous, but your
`register` or `afterRegistration` function can perform an asynchronous
`import()`, network calls, etc. You just need to make sure that the
dispose function that you return can manage the cleanup appropriately.
You can also use `getSignal()` on the state to get an AbortSignal
which may be more convenient than tapping into the dispose function.

This is similar to how you would use `React.lazy` to defer loading the
implementation to when it is used.

See the extension-vanilla-tailwind example for a demonstration for
how this can be done.
[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/extension-vanilla-tailwind?file=src%2Fmain.ts)

:::note

The Extension itself must be synchronously imported before creating the
editor. Lexical needs all editor configuration (including nodes) up front.
This is no different than without Lexical Extension. The Lexical core would
need significant changes to allow configuration changes to an editor at
runtime.

:::

## How do we prevent all of the frameworks from being loaded?

Vanilla JS and React have first class support in Lexical. Lexical Extensions
provides a path for Extensions written for either of these to be loaded into
any editor regardless of what framework the page is using.

When you depend on an Extension that depends on `ReactExtension`, you will get a runtime
error if a React provider is not present (`LexicalExtensionComposer` or
`ReactPluginHostExtension`). Think of it like an unsatisfied but required peer
dependency. This way you won't get confused if you add an extension that requires
React but it never renders correctly because React isn't set up properly.

Other frameworks can use the functionality exposed by Lexical Extensions
to provide their own integrations in a similar way. Supporting
`DecoratorNode` subclasses in multiple frameworks is not currently
supported, or planned to be supported, because I am pretty sure that
you can use mutation listeners for the same purpose. Perhaps the
decorate method could even be deprecated at some point!

## How will this interact with Lexical Dev Tools?

All of the Builder metadata is present at runtime, and is present
on the LexicalEditor instance with a well-known symbol so that the dev tools
can access it. There are many things that could be done, here are
a few examples:

- Show a diagram of the Extension tree
- Track which config came from which Extension (e.g. which node, which theme
  property, etc.)
- Track which commands/listeners/etc. were registered by which Extensions
  (would require editor instrumentation)

---

### Packages Lexical Website Docs Extensions Included Extensions (`packages/lexical-website/docs/extensions/included-extensions.md`)

# Included Extensions

[@lexical/a11y](/docs/api/modules/lexical_a11y)

Framework-agnostic accessibility extensions. See [Keyboard Accessibility](/docs/concepts/keyboard-accessibility) for the full contract and the matching `@lexical/react` adapters.

- [AriaLiveRegionExtension](/docs/api/modules/lexical_a11y#arialiveregionextension) - Owns a visually hidden `aria-live` region and exposes a stable `announce` sink (WAI-ARIA status messages, WCAG 4.1.3)
- [EditorModeAnnounceExtension](/docs/api/modules/lexical_a11y#editormodeannounceextension) - Announces editable / read-only transitions through the live region
- [FocusManagerExtension](/docs/api/modules/lexical_a11y#focusmanagerextension) - Editor ↔ toolbar focus jump (Alt+F10 to the toolbar, Escape back to the editor), reference-counted per toolbar
- [FocusTrapExtension](/docs/api/modules/lexical_a11y#focustrapextension) - Traps Tab / Shift+Tab focus inside one or more containers (modal dialogs), reference-counted per container
- [HistoryAnnounceExtension](/docs/api/modules/lexical_a11y#historyannounceextension) - Announces undo / redo through the live region
- [RovingTabIndexExtension](/docs/api/modules/lexical_a11y#rovingtabindexextension) - WAI-ARIA roving-tabindex pattern for toolbars / groups, reference-counted per container

[@lexical/clipboard](/docs/api/modules/lexical_clipboard)

- [ClipboardDOMImportExtension](/docs/api/modules/lexical_clipboard#clipboarddomimportextension) - Routes `text/html` pastes and drops through the `DOMImportExtension` pipeline (experimental)
- [ClipboardImportExtension](/docs/api/modules/lexical_clipboard#clipboardimportextension) - Configurable per-MIME-type clipboard import for paste and drop (experimental)
- [GetClipboardDataExtension](/docs/api/modules/lexical_clipboard#getclipboarddataextension) - Configurable serialization of the selection into clipboard MIME types for copy and drag

[@lexical/code](/docs/api/modules/lexical_code)

- [CodeExtension](/docs/api/modules/lexical_code#codeextension) - CodeNode (code blocks)
- [CodeIndentExtension](/docs/api/modules/lexical_code#codeindentextension) - CodeNode tab key indentation (code blocks)

[@lexical/code-prism](/docs/api/modules/lexical_code-prism)

- [CodePrismExtension](/docs/api/modules/lexical_code-prism#codeprismextension) - Highlighting with Prism for CodeNode

[@lexical/code-shiki](/docs/api/modules/lexical_code-shiki)

- [CodeShikiExtension](/docs/api/modules/lexical_code-shiki#codeshikiextension) - Highlighting with Shiki for CodeNode

[@lexical/dragon](/docs/api/modules/lexical_dragon)

- [DragonExtension](/docs/api/modules/lexical_dragon#dragonextension) - Dragon (speech to text) support, included by default with `RichTextExtension` and `PlainTextExtension`

[@lexical/extension](/docs/api/modules/lexical_extension)

- [AutoFocusExtension](/docs/api/modules/lexical_extension#autofocusextension) - Focus the editor when it is created (e.g. on page load)
- [ClearEditorExtension](/docs/api/modules/lexical_extension#cleareditorextension) - Implementation for the `CLEAR_EDITOR_COMMAND`
- [ClickAfterLastBlockExtension](/docs/api/modules/lexical_extension#clickafterlastblockextension) - Inserts a paragraph when clicking below a last block that can not accommodate the click (e.g. a DecoratorNode or TableNode)
- [DecoratorTextExtension](/docs/api/modules/lexical_extension#decoratortextextension) - DecoratorTextNode support, sets the format and CSS classes for the DOM container
- [EditorStateExtension](/docs/api/modules/lexical_extension#editorstateextension) - Provide EditorState as a signal (alternative to `registerUpdateListener`)
- [HMRExtension](/docs/api/modules/lexical_extension#hmrextension) - Preserves editor state, selection, editability and undo/redo history across Hot Module Replacement (HMR) cycles
- [HorizontalRuleExtension](/docs/api/modules/lexical_extension#horizontalruleextension) - HorizontalRuleNode (`<hr>` tag)
- [IMEExtension](/docs/api/modules/lexical_extension#imeextension) - Centralizes IME composition state as signals
- [InitialStateExtension](/docs/api/modules/lexical_extension#initialstateextension) - Sets the initial state of the editor (always included)
- [KeyboardShortcutsExtension](/docs/api/modules/lexical_extension#keyboardshortcutsextension) - Dispatches a table of keyboard shortcuts from a single compiled `KEY_DOWN_COMMAND` listener; the table is merged across the extension graph by name (experimental)
- [NestedEditorExtension](/docs/api/modules/lexical_extension#nestededitorextension) - Configures an editor as a nested editor of a parent editor (theme inheritance, optionally editable state)
- [NodeSelectionExtension](/docs/api/modules/lexical_extension#nodeselectionextension) - Tracks selection, typically for DecoratorNodes
- [NodeSelectionDataSelectedExtension](/docs/api/modules/lexical_extension#nodeselectiondataselectedextension) - Mirrors a node's `NodeSelection` membership onto its host DOM as a `data-selected` attribute so CSS can outline selected `ElementNode` hosts; configured per node type (experimental)
- [NormalizeInlineElementsExtension](/docs/api/modules/lexical_extension#normalizeinlineelementsextension) - Removes empty inline elements, included by default with `RichTextExtension` and `PlainTextExtension`
- [NormalizeTripleClickSelectionExtension](/docs/api/modules/lexical_extension#normalizetripleclickselectionextension) - Corrects over-selection after triple click events, included by default with `RichTextExtension` and `PlainTextExtension`
- [PreventSelectAllExtension](/docs/api/modules/lexical_extension#preventselectallextension) - Prevents select all (Ctrl/Cmd+A) inside input/textarea elements from selecting the editor content, included by default with `SelectBlockExtension`
- [RootElementExtension](/docs/api/modules/lexical_extension#rootelementextension) - Exposes the editor's root element as a reactive `Signal<HTMLElement | null>`
- [SelectBlockExtension](/docs/api/modules/lexical_extension#selectblockextension) - Select all (Ctrl/Cmd+A) selects the nearest block element first, pressing it again selects the whole document
- [SelectionAlwaysOnDisplayExtension](/docs/api/modules/lexical_extension#selectionalwaysondisplayextension) - Highlights selected content even when the editor is not focused
- [TabIndentationExtension](/docs/api/modules/lexical_extension#tabindentationextension) - Changes Tab key to insert tabs and indent instead of natively focusing the next field
- [WatchEditableExtension](/docs/api/modules/lexical_extension#watcheditableextension) - Exposes the editor's editable state as a reactive `Signal<boolean>`

[@lexical/hashtag](/docs/api/modules/lexical_hashtag)

- [HashtagExtension](/docs/api/modules/lexical_hashtag#hashtagextension) - HashtagNode

[@lexical/history](/docs/api/modules/lexical_history)

- [HistoryExtension](/docs/api/modules/lexical_history#historyextension) - History support (undo/redo)
- [SharedHistoryExtension](/docs/api/modules/lexical_history#sharedhistoryextension) - History sharing between a parent editor and its nested editors

[@lexical/html](/docs/api/modules/lexical_html)

- [CoreImportExtension](/docs/api/modules/lexical_html#coreimportextension) - DOM import rules for the core nodes, included implicitly by node-providing extensions (experimental)
- [DOMImportExtension](/docs/api/modules/lexical_html#domimportextension) - Extension-based replacement for `importDOM`/`DOMConversion`, rules are compiled into a dispatcher at editor build time (experimental)
- [DOMRenderExtension](/docs/api/modules/lexical_html#domrenderextension) - Overrides the DOM render and export behavior for an editor (experimental)

[@lexical/link](/docs/api/modules/lexical_link)

- [AutoLinkExtension](/docs/api/modules/lexical_link#autolinkextension) - AutoLinkNode, note that matchers have to be manually configured
- [ClickableLinkExtension](/docs/api/modules/lexical_link#clickablelinkextension) - Toggle between lexical selection and native click to open behavior for links
- [LinkExtension](/docs/api/modules/lexical_link#linkextension) - LinkNode (`<a>` tag)

[@lexical/list](/docs/api/modules/lexical_list)

- [ListExtension](/docs/api/modules/lexical_list#listextension) - ListNode, ListItemNode
- [CheckListExtension](/docs/api/modules/lexical_list#checklistextension) - Checklist support for ListNode and ListItemNode

[@lexical/mark](/docs/api/modules/lexical_mark)

- [MarkExtension](/docs/api/modules/lexical_mark#markextension) - MarkNode

[@lexical/mdast](/docs/api/modules/lexical_mdast)

Experimental micromark/mdast-based Markdown support (every extension in this package is experimental). See [Markdown with @lexical/mdast](/docs/serialization/markdown-mdast) for the full story.

- [MdastExtension](/docs/api/modules/lexical_mdast#mdastextension) - The configurable core registry, exposing both Markdown import and export
- [MdastHeadingExtension](/docs/api/modules/lexical_mdast#mdastheadingextension) - ATX & setext headings (HeadingNode)
- [MdastBlockquoteExtension](/docs/api/modules/lexical_mdast#mdastblockquoteextension) - Block quotes (QuoteNode)
- [MdastRichTextExtension](/docs/api/modules/lexical_mdast#mdastrichtextextension) - Bundle of heading + blockquote
- [MdastListExtension](/docs/api/modules/lexical_mdast#mdastlistextension) - Ordered/unordered lists (ListNode, ListItemNode)
- [MdastTaskListExtension](/docs/api/modules/lexical_mdast#mdasttasklistextension) - GFM task lists (`- [x]`), layered on MdastListExtension
- [MdastCodeExtension](/docs/api/modules/lexical_mdast#mdastcodeextension) - Fenced & indented code blocks (CodeNode)
- [MdastLinkExtension](/docs/api/modules/lexical_mdast#mdastlinkextension) - Links, CommonMark autolinks, reference links (LinkNode)
- [MdastAutolinkLiteralExtension](/docs/api/modules/lexical_mdast#mdastautolinkliteralextension) - GFM literal autolinks (bare URLs in prose)
- [MdastHorizontalRuleExtension](/docs/api/modules/lexical_mdast#mdasthorizontalruleextension) - Thematic breaks (HorizontalRuleNode)
- [MdastStrikethroughExtension](/docs/api/modules/lexical_mdast#mdaststrikethroughextension) - GFM strikethrough (text format)
- [MdastTableExtension](/docs/api/modules/lexical_mdast#mdasttableextension) - GFM tables (TableNode, ...)
- [MdastCommonMarkExtension](/docs/api/modules/lexical_mdast#mdastcommonmarkextension) - Bundle of the six CommonMark extensions
- [MdastGfmExtension](/docs/api/modules/lexical_mdast#mdastgfmextension) - Bundle of the four GFM extensions
- [MdastShadowRootQuoteExtension](/docs/api/modules/lexical_mdast#mdastshadowrootquoteextension) - Opt-in: blockquotes as block containers with full-fidelity nested content
- [MdastHtmlExtension](/docs/api/modules/lexical_mdast#mdasthtmlextension) - Opt-in: raw HTML in Markdown routed through the `@lexical/html` DOM import rules (Markdown inside the tags included, GitHub-style), plus generic HTML-encoded export via `$exportViaDOM` / `rawHtmlBlock`
- [MdastShortcutsExtension](/docs/api/modules/lexical_mdast#mdastshortcutsextension) - Streaming Markdown typing shortcuts, driven by the same grammar as import

[@lexical/overflow](/docs/api/modules/lexical_overflow)

- [OverflowExtension](/docs/api/modules/lexical_overflow#overflowextension) - OverflowNode

[@lexical/plain-text](/docs/api/modules/lexical_plain-text)

- [PlainTextExtension](/docs/api/modules/lexical_plain-text#plaintextextension) - Plain text editor, the return key creates a LineBreakNode by default (one ParagraphNode per document)

`@lexical/react`

- [ReactExtension](/docs/api/modules/lexical_react_ReactExtension#reactextension)
- [ReactPluginHostExtension](/docs/api/modules/lexical_react_ReactPluginHostExtension#reactpluginhostextension)
- [ReactProviderExtension](/docs/api/modules/lexical_react_ReactProviderExtension#reactproviderextension)
- [TreeViewExtension](/docs/api/modules/lexical_react_TreeViewExtension#treeviewextension)

[@lexical/rich-text](/docs/api/modules/lexical_rich-text)

- [RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) - Rich Text editor (QuoteNode, HeadingNode), the return key creates a ParagraphNode by default (multiple ParagraphNode per document). Includes configurable `escapeFormatTriggers` to escape text formatting (e.g. code) at text node boundaries, and `shouldHandlePasteAsFiles` to control whether files win over incidental text/HTML clipboard entries on paste

[@lexical/table](/docs/api/modules/lexical_table)

- [TableExtension](/docs/api/modules/lexical_table#tableextension) - TableNode, TableRowNode, TableCellNode

[@lexical/tailwind](/docs/api/modules/lexical_tailwind)

- [TailwindExtension](/docs/api/modules/lexical_tailwind#tailwindextension) - A theme configuration that uses Tailwind classes

---

### Packages Lexical Website Docs Extensions Migration (`packages/lexical-website/docs/extensions/migration.mdx`)

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Migration Guide

Migrating to Lexical Extensions is designed to be seamless! Lexical Extensions is
a higher-level API on top of the existing functionality you are already using.

Generally speaking, the only thing that needs to change is how you create the
editor. Everything else can be migrated (or not) at your own leisure, but
the result will be simpler and more composable if you do!

## Vanilla JS App

<Tabs>
<TabItem value="before" label="Before">

```ts
import {registerDragonSupport} from '@lexical/dragon';
import {createEmptyHistoryState, registerHistory} from '@lexical/history';
import {HeadingNode, QuoteNode, registerRichText} from '@lexical/rich-text';
import {mergeRegister} from '@lexical/utils';
// highlight-next-line
import {createEditor} from 'lexical';
import $prepopulatedRichText from './$prepopulatedRichText';

const editorRef = document.getElementById('lexical-editor');
const stateRef = document.getElementById(
  'lexical-state',
) as HTMLTextAreaElement;

// highlight-next-line
const editor = createEditor({
  namespace: 'Vanilla JS Demo',
  // Register nodes specific for @lexical/rich-text
  nodes: [HeadingNode, QuoteNode],
  // highlight-start
  onError: (error: Error) => {
    throw error;
  },
  // highlight-end
  theme: {quote: 'PlaygroundEditorTheme__quote'},
});
editor.setRootElement(editorRef);

// Registering Plugins
mergeRegister(
  registerRichText(editor),
  registerDragonSupport(editor),
  registerHistory(editor, createEmptyHistoryState(), 300),
);

editor.update($prepopulatedRichText, {tag: 'history-merge'});
```

</TabItem>
<TabItem value="minimal" label="After (minimal changes)">

```ts
import {registerDragonSupport} from '@lexical/dragon';
import {createEmptyHistoryState, registerHistory} from '@lexical/history';
import {HeadingNode, QuoteNode, registerRichText} from '@lexical/rich-text';
import {mergeRegister} from '@lexical/utils';
// highlight-next-line
import {buildEditorFromExtensions} from '@lexical/extension';
import $prepopulatedRichText from './$prepopulatedRichText';

const editorRef = document.getElementById('lexical-editor');
const stateRef = document.getElementById(
  'lexical-state',
) as HTMLTextAreaElement;

// highlight-next-line
const editor = buildEditorFromExtensions({
  // Any string is suitable as long as it uniquely defines the Extension in the editor
  name: '[root]',
  namespace: 'Vanilla JS Demo (with Lexical Extension)',
  // Register nodes specific for @lexical/rich-text
  nodes: [HeadingNode, QuoteNode],
  // highlight-next-line
  // onError boilerplate removed
  theme: {quote: 'PlaygroundEditorTheme__quote'},
});
editor.setRootElement(editorRef);

// Registering Plugins
mergeRegister(
  registerRichText(editor),
  registerDragonSupport(editor),
  registerHistory(editor, createEmptyHistoryState(), 300),
);

editor.update($prepopulatedRichText, {tag: 'history-merge'});
```

</TabItem>
<TabItem value="all-in" label="After (all-in)">

```ts
import {buildEditorFromExtensions} from '@lexical/extension';
import {HistoryExtension} from '@lexical/history';
import {RichTextExtension} from '@lexical/rich-text';
import $prepopulatedRichText from './$prepopulatedRichText';

const editorRef = document.getElementById('lexical-editor');

const editor = buildEditorFromExtensions({
  // highlight-start
  // This works similarly to LexicalComposer editorState
  $initialEditorState: $prepopulatedRichText,
  // highlight-end
  name: '[root]',
  namespace: 'Vanilla JS Demo (all-in with Lexical Builder)',
  // highlight-start
  // RichTextExtension has a nodes property to add QuoteNode and HeadingNode
  // DragonExtension is a dependency of RichTextExtension
  // All three extensions have register properties to add behavior to the editor,
  // with defaults for all of the History configuration
  dependencies: [RichTextExtension, HistoryExtension],
  // highlight-end
  theme: {quote: 'PlaygroundEditorTheme__quote'},
});
editor.setRootElement(editorRef);
```

</TabItem>
</Tabs>

## React App

<Tabs>
<TabItem value="before" label="Before">

```
/* Detailed source-code truncated for AI context efficiency. */
```

</TabItem>
<TabItem value="after" label="After">

```
/* Detailed source-code truncated for AI context efficiency. */
```

</TabItem>
</Tabs>

## React Plug-in (without UI)

<Tabs>
<TabItem value="before" label="Before">

```
/* Detailed source-code truncated for AI context efficiency. */
```

</TabItem>
<TabItem value="minimal" label="After (minimal)">

```
/* Detailed source-code truncated for AI context efficiency. */
```

</TabItem>
<TabItem value="all-in" label="After (all-in)">

```
/* Detailed source-code truncated for AI context efficiency. */
```

</TabItem>
</Tabs>

---

### Packages Lexical Website Docs Extensions Peer Dependencies (`packages/lexical-website/docs/extensions/peer-dependencies.md`)

# Peer Dependencies

Peer Dependencies are used to declare indirect or optional dependencies
between extensions. This can be used to enable some functionality or
configuration only when some other extension is available, or to
avoid a dependency loop. Extensions must form a directed acyclic graph,
there must not be a dependency path from one extension back to itself.

## Use Cases

Generally the intended use case for `peerDependencies` is to provide
functionality only when some other extension is present. For example,
you may have an extension that requires special code to interact with
React, but is also usable without React, so you could have
`ReactProviderExtension` in the `peerDependencies` array.

Another example would be to provide Markdown serialization or import
for an extension that provides some custom node type, but only
if the `MdastExtension` is present in that editor.

---

### Packages Lexical Website Docs Extensions React (`packages/lexical-website/docs/extensions/react.md`)

# React and Lexical Extension

Lexical Extension vastly improved support for non-React usage, but
did not make any compromises when supporting React.

## Minimal migration to LexicalExtensionComposer

Migrating your application from `LexicalComposer` to
`LexicalExtensionComposer` is straightforward, but has
two things to consider.

### Convert your `initialConfig` to an `extension`

Before:
```tsx
const initialConfig = {
  namespace: 'MyEditor',
  nodes: [QuoteNode, HeadingNode],
  theme,
  editorState(editor) {
    editor.update($initialEditorState, {tag: HISTORY_MERGE_TAG});
  },
  // This is the default when using Lexical Extension
  onError(err) {
    throw err;
  },
};
```

After:
```tsx
// Be careful to make sure that this is a stable reference, either by defining it
// at the top-level of a module or by using something like `useRef` or `useState`
// to create it. Any time this object changes, the editor will be recreated.
const appExtension = defineExtension({
  name: 'MyEditor',
  namespace: 'MyEditor',
  nodes: () => [QuoteNode, HeadingNode],
  theme,
  $initialEditorState,
});
```

### Switch from LexicalComposer to LexicalExtensionComposer

Before:
```tsx
<LexicalComposer initialConfig={initialConfig}>
    <RichTextPlugin
        contentEditable={<ContentEditable />}
        ErrorBoundary={LexicalErrorBoundary}
    />
    {/* other legacy React plugins */}
</LexicalComposer>
```

After:
```tsx
<LexicalExtensionComposer extension={appExtension} contentEditable={null}>
    <RichTextPlugin
        contentEditable={<ContentEditable />}
        ErrorBoundary={LexicalErrorBoundary}
    />
    {/* other legacy React plugins */}
</LexicalExtensionComposer>
```

This is a minimal migration, in most cases you can migrate plug-in usage to
extension dependencies. For example, by using
[RichTextExtension](/docs/api/modules/lexical_rich-text#richtextextension) instead of
`RichTextPlugin`:

```tsx
const appExtension = defineExtension({
  name: 'MyEditor',
  namespace: 'MyEditor',
  dependencies: [RichTextExtension],
  theme,
  $initialEditorState,
});
```

```tsx
<LexicalExtensionComposer extension={appExtension}>
  {/* By default the contentEditable is the first child, defaulting to <ContentEditable /> */}
  {/* other legacy React plugins */}
</LexicalExtensionComposer>
```

## Managing the editor yourself with LexicalExtensionEditorComposer

`LexicalExtensionComposer` builds its editor from `extension` and disposes it
when the component unmounts. When you want to own that lifecycle instead,
such as a nested editor that belongs to a node (like the caption in the
playground's `StickyComponent`), an editor shared with non-React code, or one
that should outlive the component, build the editor yourself and render it
with
[`LexicalExtensionEditorComposer`](/docs/api/modules/lexical_react_LexicalExtensionEditorComposer).

The editor must include `ReactProviderExtension` and `ReactExtension`, which
`LexicalExtensionComposer` otherwise adds for you:

```tsx
import {buildEditorFromExtensions, defineExtension} from '@lexical/extension';
import {LexicalExtensionEditorComposer} from '@lexical/react/LexicalExtensionEditorComposer';
import {ReactExtension} from '@lexical/react/ReactExtension';
import {ReactProviderExtension} from '@lexical/react/ReactProviderExtension';
import {RichTextExtension} from '@lexical/rich-text';

// Built once, at module scope, so it is not re-created on every render
const editor = buildEditorFromExtensions(
  defineExtension({
    dependencies: [ReactProviderExtension, ReactExtension, RichTextExtension],
    name: '@my-app/editor',
  }),
);

function App() {
  return (
    <LexicalExtensionEditorComposer initialEditor={editor}>
      {/* The ContentEditable is rendered by ReactExtension, as with LexicalExtensionComposer */}
      {/* other legacy React plugins */}
    </LexicalExtensionEditorComposer>
  );
}
```

`LexicalExtensionEditorComposer` never disposes the editor, so call
`editor.dispose()` yourself when you are done with it. Keep the
`initialEditor` prop stable (module scope, `useMemo`, or state held by a
node), and render a given editor in only one place at a time, since an editor
has only one root element.

## React Plug-ins (Legacy)

These just work as-is, by rendering them as children of your composer. You may
wish to package them as an extension for easier re-use, or consider refactoring
them to work without React. See [Extension Decorators](#extension-decorators)
and [Output Components](#output-components), and
[React Independence](#react-independence) for strategies to repackage or
refactor your legacy plug-ins.

## Extension Decorators

A common use case for legacy react plug-ins is to render some React component,
but the position of that component in the document is not important. Some use
cases for this are when the component is simply an effect that always returns
`null`, or it is rendering something like a portal. There's no need for users
to manually decide where these components are rendered, so Lexical's
ReactExtension maintains an array of these to render automatically.

 ```tsx
 // This is exactly how you would write a legacy React plug-in
function LogEditorPlugin() {
  const [editor] = useLexicalComposerContext()
  useEffect(() => {
    console.log(editor);
  }, [editor]);
  return null;
}
// This is how you would declare the plugin as an extension
// that depends on React and renders this plug-in as a decorator
export const LogEditorExtension = defineExtension({
  name: "@example/LogEditor",
  dependencies: [
    configExtension(ReactExtension, {
      decorators: [<LogEditorPlugin />]
    }),
  ],
});
```

## Output Components

In situations where it is important for the user to decide exactly where an
extension's component is rendered in the React document, the convention is
to provide an Output Component.

Here we have the TreeViewExtension which defines an Output Component to render
the TreeView with the specified configuration.

```tsx
export function TreeViewExtensionComponent(
  props: Partial<TreeViewConfig>,
): JSX.Element {
  const [editor] = useLexicalComposerContext();
  return (
    <TreeView
      editor={editor}
      {...useExtensionDependency(TreeViewExtension).config}
      {...props}
    />
  );
}

export const TreeViewExtension = defineExtension({
  build: () => ({Component: TreeViewExtensionComponent}),
  config,
  dependencies: [ReactExtension],
  name: '@lexical/react/TreeView',
});
```

This extension component can be acquired with the
[useExtensionComponent](/docs/api/modules/lexical_react_useExtensionComponent#useextensioncomponent)
hook or the [ExtensionComponent](/docs/api/modules/lexical_react_ExtensionComponent#extensioncomponent) component.

```tsx
return (
  <ExtensionComponent
    lexical:extension={TreeViewExtension}
    viewClassName="tree-view-output" />
);
```

```tsx
const TreeViewComponent = useExtensionComponent(TreeViewExtension);
return (<TreeViewComponent viewClassName="tree-view-output" />);
```

## React Independence

Many legacy plug-ins are merely a `useEffect` wrapper, these tend to not
need React at all. For example:

Before:
```tsx
export function CheckListPlugin(): null {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    return registerCheckList(editor);
  }, [editor]);
  return null;
}
```

After:
```tsx
export const CheckListExtension = defineExtension({
  name: "@lexical/list/CheckList",
  register: registerCheckList,
});
```

## Using React extensions and plug-ins without JSX

[ReactPluginHostExtension](/docs/api/modules/lexical_react_ReactPluginHostExtension#reactpluginhostextension)
is an extension that allows you to mount the a React root at a
specific DOM element in your app so that applications that are
not natively React can still take advantage of existing legacy
plug-ins and extensions that depend on React.

While this does mean your application will include the React runtime, you
don't have to use JSX or have any other React infrastructure in your app.

See
[extension-vanilla-react-plugin-host](https://github.com/facebook/lexical/blob/main/examples/extension-vanilla-react-plugin-host/src/main.ts)
[](https://stackblitz.com/github/facebook/lexical/tree/main/examples/?file=src%2Fmain.ts)
for usage of this extension and its API

---

### Packages Lexical Website Docs Extensions Signals (`packages/lexical-website/docs/extensions/signals.md`)

# Signals

Lexical Extensions uses signals as the reactive interface that you can use to
communicate with and between extensions.

## Motivation

At the lowest level, communicating between parts of Lexical is done with
callbacks such as `registerCommand`, `registerUpdateListener`,
`registerMutationListener`, and `addEventListener` with the DOM.

How these callbacks behave and in some cases whether they are registered
at all depends on state local to the extension that may also be mutable,
so would require a similar callback mechanism with a naïve implementation.
With legacy React plug-ins, this mechanism is `useEffect` which is very
coarse and doesn't really have a portable equivalent when using other
or no framework.

The initial prototype was largely callback based as well, but this quickly
becomes very difficult to work with when you have to do work based on
the "result" of multiple callbacks or configuration values. Signals are
a more general solution to this problem. There's even a
[TC39 Signals Proposal](https://github.com/tc39/proposal-signals) to add
infrastructure for this to the JavaScript standard.

Signals also allow for optimizations by reducing redundant callbacks with
primitives such as `batch` or by allowing the current value to be inspected
with `peek` without adding any subscription. Basically, the same Signal can
be used either like React State when you want to run code as the value changes
over time, or like a React Ref when you only need to know the current value
(as is typical with event handlers).

Signals also do not require you to manage a dependency array or use a
compiler, the subscriptions and dependencies are inferred at runtime
as they are used. This means that the set of signals subscribed to by a
given effect can change without violating any rules or generating any
warnings.

## Implementation

Since Signals are not yet standardized, and the
[signal-polyfill](https://github.com/proposal-signals/signal-polyfill) is not
ready for production use, it was decided to leverage
[@preact/signals-core](https://github.com/preactjs/signals). This
implementation is dependency-free, optimized, and well tested.

For future compatibility we *do not recommend* that you use
`@preact/signals-core` directly, and instead use the re-exports from
[@lexical/extension](https://lexical.dev/docs/api/modules/lexical_extension).

As of the initial release, these re-exports are:

```ts
export {
  batch,
  computed,
  effect,
  type ReadonlySignal,
  type Signal,
  signal,
  type SignalOptions,
  untracked,
} from '@preact/signals-core';
```

## Usage

For thorough examples on how the signal primitives work, you can read through the
[Preact Signals](https://preactjs.com/guide/v10/signals/) documentation.

Lexical provides the following higher-level signal functions to make it easier
to build extensions:

### namedSignals

[namedSignals](/docs/api/modules/lexical_extension#namedsignals)
is a convenience function used to output independent signals from
each property of a configuration object.

```ts
export interface TabIndentationConfig {
  disabled: boolean;
  maxIndent: null | number;
  /**
   * By default, indents are set on all elements for which the
   * {@link ElementNode.canIndent} returns true. This option allows you to set
   * indents for specific nodes without overriding the method for others.
   */
  $canIndent: CanIndentPredicate;
}

export const TabIndentationExtension = defineExtension({
  build(editor, config, state) {
    return namedSignals(config);
  },
  config: safeCast<TabIndentationConfig>({
    $canIndent: $defaultCanIndent,
    disabled: false,
    maxIndent: null,
  }),
  name: '@lexical/extension/TabIndentation',
  register(editor, config, state) {
    const {disabled, maxIndent, $canIndent} = state.getOutput();
    return effect(() => {
      if (!disabled.value) {
        // This register function's implementation uses peek on the maxIndent
        // signal to get its current value when needed, no need to re-register
        // every time this value changes.
        return registerTabIndentation(editor, maxIndent, $canIndent);
      }
    });
  },
});
```

### watchedSignal

[watchedSignal](/docs/api/modules/lexical_extension#watchedsignal)
is equivalent to React's [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)

```ts
export const EditorStateExtension = defineExtension({
  build(editor) {
    return watchedSignal(
      () => editor.getEditorState(),
      (editorStateSignal) =>
        editor.registerUpdateListener((payload) => {
          editorStateSignal.value = payload.editorState;
        }),
    );
  },
  name: '@lexical/extension/EditorState',
});
```

---

--- METRICS ---
- Files Extracted: 112
- Estimated Token Budget: ~125471 tokens
- Canonical Reference: https://codewiki.google/github.com/facebook/lexical
