v0.52.0 is an ad-hoc release targeting a few fixes to mobile text input, but carries a number of other updates accumulated in the past week as we move towards Lexical 1.0. We did a pass to modernize the documentation focusing on the latest features. @lexical/mdast now uses one configurable MdastExtension for import and export, headless editors can be built entirely from extensions, and selection notifications have a consistent timing contract. There are also fixes for deletion, horizontal caret scrolling, drag-and-drop and mobile input.
Breaking Changes
lexical—SELECTION_CHANGE_COMMANDconsistently runs before DOM reconciliation, inside the pending writable update. This includes automaticNodeSelectionandTableSelectionnotifications that previously ran after reconciliation.$getSelection()returns the pending selection;$getPreviousSelection()returns the last committed selection. Eligible programmatic changes now notify without waiting for a nativeselectionchangeevent. Listener edits join the same commit, and handlers that change the selection must converge. A listener that throws can now abort the whole pending update, including the edit that triggered the notification, and listeners inherit that update's tags, so content edits made during aCOLLABORATION_TAGorHISTORIC_TAGupdate are not synced to Yjs peers. Move DOM-dependent work such as element lookup, focus checks and floating UI positioning into$onUpdate(() => editor.read('latest', () => { /* ... */ })), reading selection and nodes inside that callback. UseregisterUpdateListenerfor UI that must respond to every commit, including content-only changes (#9219, selection timing guide)lexical/@lexical/html— Default HTML export now honorsDOMRenderExtension$createDOMoverrides. This affects nodes that inheritLexicalNode.exportDOMor callsuper.exportDOM(editor), including text and paragraphs. If an override was intended only for the live editor, review the HTML it now exports. CustomexportDOMimplementations that create their own elements can still bypass this hook;$updateDOMand$decorateDOMremain reconciliation-only, and$exportDOMhandles export-specific changes (#9231)lexical—LexicalNode.getCommonAncestoris removed. Use$getCommonAncestor(a, b)?.commonAncestor ?? null. This is not quite a direct substitution: the removed method first replaced non-element arguments with their parents, while the function compares the supplied nodes and can return a text node when both arguments are that node. Pass the parents of non-element nodes if you need the old behavior, and use type guards instead of the old unchecked generic type argument (#9238)
Deprecations
@lexical/mdast—MdastImportExtensionandMdastExportExtensionare deprecated aliases ofMdastExtension. Move configuration toMdastExtensionand replaceMdastImportExtensionOutput/MdastExportExtensionOutputwithMdastExtensionOutput. The package is experimental, and these aliases are intended to remain for only one release (#9198)
New APIs & Features
@lexical/mdast— Unified Markdown configuration and inherited export rules.MdastExtensionowns both import and export, and feature extensions include it automatically. Export rules accept a node class or type string and apply to subclasses, with more specific rules taking precedence. ATextNoderule can therefore handle tabs and custom text subclasses without separate registrations (#9198)@lexical/mdast— Import/export middleware throughcontext.next(). Delegate conversion of the same node to the remaining handlers, then return or modify their output. Export chains try remaining same-type rules before ancestor rules. Usecontext.next()to delegate:nullretains its existing meaning of default export or omitted import, while[]omits content in either direction. Formatted text from middleware participates in text merging; custom structure or metadata is preserved as-is (#9198)@lexical/headless—HeadlessExtensionworks withbuildEditorFromExtensionsfor server-side processing and tests. It marks the editor as headless and prevents attaching a DOM root. UnlikecreateHeadlessEditor, it keeps listener registration methods available, so extensions that register root listeners can still be used (#9249)
Notable Fixes
Selection & editing
- Backspace over all text in the first block preserves the empty block and its type (#9236); select-all deletion restores an editable paragraph when the root still contains slots such as footnotes (#9240); line deletion correctly resolves endpoints inside text-bearing inline decorators (#9247)
- Dropping dragged text on the edge of its own selection no longer deletes it (#9241); persistent selection highlights retain coverage across mixed font sizes and partially overlapping rectangles (#9196)
- Lexical-controlled caret moves reveal the caret horizontally in code blocks and other scrolling containers, including table wrappers. Horizontal scrolling respects
scroll-padding, with updated playground styling to keep the caret clear of the code gutter (#9215) - Focus commands triggered during DOM selection synchronization no longer produce spurious read-only warnings; dispatching commands from an explicit read context still warns (#9230)
Mobile input & recovery
- iOS tracks explicit Shift key events so Shift+Enter can insert a line break without treating automatic capitalization as Shift. Ordinary Enter continues to insert paragraphs; when capitalization has already highlighted Shift, it must be toggled off and on to request a line break (#9242)
- The input-suppression workaround after handled Backspace or select-all is limited to macOS Chromium, preventing legitimate text input from being discarded on other platforms (#9251)
setEditorStatewith a childless root warns and recovers to an empty paragraph in production. Development builds still report the invalid state throughonError(#9183)
Performance & signals
- Writable nodes are cached during pending updates, reducing repeated node-map lookups (#9169)
@lexical/extensionraises its@preact/signals-coredependency to^1.14.4, fixing stale computed values after a batch changes a signal and restores its original value (#9248)
Playground & docs
- Getting-started guides now use module-level root extensions, with five live editors, examples drawn directly from runnable source, and Open in StackBlitz buttons. Custom-feature examples demonstrate
$configschemas, NodeState, and HTML import/export through extensions (#9237) - The introduction, Concepts and Serialization guides have been rewritten around current APIs, with Mermaid diagrams, an interactive update lifecycle explanation and a dedicated comparison with ProseMirror. Older setup and serialization APIs remain documented in Legacy sections (#9249)
- Numerous API names, snippets and lifecycle descriptions have been corrected throughout the docs, most of them by @potatowagon. API reference links now point to the right GitHub source paths and resolve external types (#9204, #9220)
- Playground Find & Replace calculates offsets correctly across decorators and slots (#9186)
What's Changed
- v0.51.0 by @etrepum in #9178
- [lexical] Refactor: Cache writable nodes during pending updates by @etrepum in #9169
- [lexical-website] Documentation Update: fix $insertList argument in the commands guide by @potatowagon in #9188
- [lexical-website] Documentation Update: seed the editor state with setEditorState, not registerRichText by @potatowagon in #9189
- [lexical-website] Documentation Update: title the history page after its public API, not useHistory by @potatowagon in #9190
- [lexical-website] Documentation Update: use the real plugin component names in the editor-state React example by @potatowagon in #9191
- [lexical-website] Documentation Update: getElementByKey returns a DOM element, not a node by @potatowagon in #9192
- [lexical] Bug Fix: recover from an empty editor state instead of throwing by @potatowagon in #9183
- [lexical-playground] Bug Fix: Fix Find & Replace offsets across decorators and slots by @Monier-Ayman in #9186
- [lexical-website] Documentation Update: the deserialization hook is importJSON, not importFromJSON by @potatowagon in #9199
- [lexical-website] Documentation Update: useReactDecorators, and the three missing TextNode formats by @potatowagon in #9201
- [lexical-website] Documentation Update: clone is synthesized for $config nodes, and fix a setSomeData typo by @potatowagon in #9202
- [lexical-website] Documentation Update: node-replacement config uses nodes:, not nodes= by @potatowagon in #9203
- [lexical-website] Bug Fix: Include packages/ in API docs GitHub source links by @etrepum in #9204
- [lexical-website] Documentation Update: fix syntax errors, outdated APIs, and broken links in Node Transforms guide by @jaideepkrishna2008-ui in #9197
- [lexical-code-core][lexical-code-prism][lexical-code-shiki] Refactor: move the duplicated updateCodeGutter into @lexical/code-core by @Om-singhaI in #9164
- [lexical-mdast] Feature: Unify MdastExtension and inherit export rules by @etrepum in #9198
- [lexical-website] Documentation Update: fix two nodes.mdx snippets that do not compile by @potatowagon in #9200
- [lexical-website] Documentation Update: show $initialEditorState, not a function passed to createEditor by @potatowagon in #9205
- [lexical-website] Documentation Update: correct the shadow-DOM support table and two API references by @potatowagon in #9206
- [lexical-website] Documentation Update: fix the test path in the traversals maintenance comment by @potatowagon in #9207
- [lexical-website] Documentation Update: correct four claims in the extension lifecycle guide by @potatowagon in #9208
- [lexical-website] Documentation Update: re-sync the TabIndentationExtension snippet with its source by @potatowagon in #9209
- [lexical-website] Documentation Update: fix the broken snippets in the extensions migration guide by @potatowagon in #9211
- [lexical-website] Documentation Update: call the system Lexical Extensions, not Lexical Builder by @potatowagon in #9212
- [lexical-website] Documentation Update: fix two extension names that do not exist by @potatowagon in #9210
- [lexical-website] Documentation Update: add HMRExtension and KeyboardShortcutsExtension to the included list by @potatowagon in #9213
- [lexical-website] Documentation Update: fix seven incorrect API references in the DOM import guide by @potatowagon in #9222
- [lexical-website] Documentation Update: correct DOMExportOutput, DOMConversion priority, and the HeadingNode examples by @potatowagon in #9223
- [lexical-website] Documentation Update: fix the collaboration snippets and two stale playground references by @potatowagon in #9225
- [lexical-website] Documentation Update: correct five stale facts in the maintainers guide by @potatowagon in #9226
- [lexical-website] Documentation Update: setEditorState warns and recovers in production, it does not always throw by @potatowagon in #9228
- [lexical-website] Documentation Update: point the selection guide at NodeCaret by @potatowagon in #9229
- [lexical-website] Bug Fix: resolve external API links and shorten submodule labels by @etrepum in #9220
- [lexical] Bug Fix: Avoid read-only warnings for commit-time focus commands by @etrepum in #9230
- [lexical][lexical-html] Breaking Change: honor $createDOM overrides in default DOM export by @etrepum in #9231
- [lexical-website] Documentation Update: document the full update-listener payload and the cascade guard by @potatowagon in #9227
- [lexical-compiler] Bug Fix: Normalize temporary paths in watcher tests by @minwookshin in #9195
- [lexical][lexical-table][lexical-playground][lexical-website] Breaking Change: Standardize selection notification timing by @etrepum in #9219
- [benchmarks] Chore: Migrate benchmarks to vitest 5 and add type checking in CI by @etrepum in #9235
- [lexical][lexical-code-core][lexical-playground] Bug Fix: reveal the caret in code blocks and other containers that scroll sideways by @Om-singhaI in #9215
- [lexical] Bug Fix: Preserve first block after backward range deletion by @Abdul-Azhar-Jamesh in #9236
- [lexical-selection][lexical-utils] Bug Fix: Preserve selection coverage across mixed typography by @minwookshin in #9196
- [lexical-clipboard] Bug Fix: Keep dragged text when it is dropped on the edge of its own selection by @kwy404 in #9241
- [lexical] Breaking change: Remove LexicalNode.getCommonAncestor by @mayrang in #9238
- [lexical] Bug Fix: Handle select-all deletion with root slots by @etrepum in #9240
- [lexical-website] Documentation Update: Document ErrorBoundary configuration for PlainTextExtension by @dvd233 in #9182
- [lexical] Bug Fix: resolve line deletion endpoints inside inline decorators by @maximilliangrand in #9247
- [lexical] Bug Fix: Limit handled selection input suppression to macOS Chrome by @etrepum in #9251
- [lexical-playground] Chore: Harden nested editor selection ownership test by @etrepum in #9243
- [lexical-extension] Chore: Update signals-core to 1.14.4 by @etrepum in #9248
- [lexical-website][examples] Documentation: Modernize the getting-started section by @etrepum in #9237
- [lexical] Bug Fix: Track explicit Shift state for iOS line breaks by @etrepum in #9242
- [lexical][lexical-headless][lexical-website] Documentation Update: Rewrite the introduction and modernize the Concepts, Serialization docs and examples by @etrepum in #9249
New Contributors
- @Monier-Ayman made their first contribution in #9186
- @jaideepkrishna2008-ui made their first contribution in #9197
- @minwookshin made their first contribution in #9195
- @Abdul-Azhar-Jamesh made their first contribution in #9236
- @kwy404 made their first contribution in #9241
- @dvd233 made their first contribution in #9182
- @maximilliangrand made their first contribution in #9247
Full Changelog: v0.51.0...v0.52.0