Download Latest Version v0.48.0 source code.zip (7.7 MB)
Email in envelope

Get an email when there's a new version of Lexical

Home / v0.46.0
Name Modified Size InfoDownloads / Week
Parent folder
README.md 2026-06-25 34.9 kB
v0.46.0 source code.tar.gz 2026-06-25 6.3 MB
v0.46.0 source code.zip 2026-06-25 7.4 MB
Totals: 3 Items   13.7 MB 0

v0.46.0 is a monthly release headlined by two major new experimental capabilities:

  • Named slots - a model that lets a single host element or decorator node own several isolated editable regions inside the host's own editorState
  • Shadow DOM - the root element can now be hosted in an open shadow root (or iframe)

This release also includes many other fixes and new features across IME/composition, collaboration, markdown round-tripping, lists, code blocks, links, and mobile keyboards. It is also our first release (other than nightlies) to use NPM Trusted Publishing.

Special recognition for this release goes to @mayrang for doing the majority of the work on both of these new features as well as fixing some tricky IME/composition issues 👏 @levensta has also been doing a fantastic job going through the issue backlog, providing valuable feedback on these bleeding edge features, and identifying long-standing edge cases.

Breaking Changes

lexical — Node traversal methods no longer accept unsafe type parameters (#8661)

The zero-argument generics on getParent(OrThrow), getPreviousSibling(s), getNextSibling(s), getChildren, getFirstChild(OrThrow), getLastChild(OrThrow), getChildAtIndex, getFirstDescendant, getLastDescendant, and getDescendantByIndex were implicit unchecked casts — there is no inference site, so any non-base type argument was equivalent to an as cast. Each method is now an overload pair: a documented non-generic signature returning the base type (LexicalNode / ElementNode | null), plus the old generic signature marked @deprecated. Code that leaned on contextual-type inference no longer type-checks:

:::ts
// Before: compiled (unsound). After: type error.
const node: ParagraphNode | null = $getRoot().getFirstChild();

// Port directly with an explicit cast (no behavior change):
const node = $getRoot().getFirstChild() as ParagraphNode | null;

// Or, better, narrow with a guard:
const node = $getRoot().getFirstChild();
if ($isParagraphNode(node)) { /* node: ParagraphNode */ }

Existing getFirstChild<ParagraphNode>()-style calls still compile but are now deprecated and will be removed in a future release. This is a types-only change with no runtime impact.

lexical — $getNearestNodeFromDOMNode(rootElement) now returns RootNode (#8588)

The "selection captured outside of Lexical" mechanism is generalized beyond DecoratorNode subtrees: setDOMUnmanaged(dom, {captureSelection: true}) now marks any subtree (e.g. a DOMRenderExtension override or a getDOMSlot widget) as selection-captured, and isDOMCapturingSelection(dom) walks ancestors so a descendant <input> reports as captured. As part of this, the root element now carries a __lexicalKey_* stash, so $getNodeFromDOM / $getNearestNodeFromDOMNode resolve the root element to the RootNode instead of null. Two call sites become more correct (drop-on-root in clipboard, table-selection→range conversion), but external callers that relied on $getNearestNodeFromDOMNode(rootElement) returning null must update. The internal-only $isSelectionCapturedInDecorator was removed.

lexical — insertNodes preserves a leading linebreak; managed <br>s are now tagged (#8615)

RangeSelection.insertNodes now preserves the first LineBreakNode when inserting inline content ahead of a block element, instead of silently dropping it. Separately, the reconciler-inserted "managed" line breaks (the otherwise-invisible <br>s Lexical adds so the caret can land in empty lines) are now identifiable in the DOM as <br data-lexical-managed-linebreak="true"> — and, on iOS Safari, an analogous <img data-lexical-managed-linebreak="true" …> in some cases. DOM/HTML snapshot test expectations may need to ignore or expect this attribute. Closes [#3980].

@lexical/html and node extensions — DOMImportExtension rules now register implicitly (#8662) (experimental)

The experimental DOM-import pipeline no longer requires you to list a per-package import extension. Each node-providing extension (RichTextExtension, ListExtension, LinkExtension, TableExtension, CodeExtension) now registers its own import rules, and the standalone RichTextImportExtension / ListImportExtension / LinkImportExtension / TableImportExtension / CodeImportExtension / HorizontalRuleImportExtension become deprecated aliases that may be removed as early as v0.47.0. Rules stay inert unless HTML is routed through ClipboardDOMImportExtension or $generateNodesFromDOMViaExtension; the legacy importDOM paste path is unchanged. If you had not adopted these v0.45.0 extensions yet, there is no breaking change.

lexical / @lexical/yjs / @lexical/clipboard / @lexical/html — Named slots (#8603) (experimental)

All slot machinery is opt-in and gated (an editor latches _slotsUsed on the first $setSlot), so editors that never use slots take identical code paths to before. Two changes are observable regardless:

  • syncLexicalUpdateToYjsV2__EXPERIMENTAL takes a new dirtyLeaves parameter, inserted between dirtyElements and normalizedNodes (a slot host's values surface as dirty leaves).
  • A NodeSelection containing an ElementNode now includes that element's children on copy/export ($getHtmlContent / $generateJSONFromSelectedNodes). The previous behavior serialized a childless shell, making cut of an element NodeSelection silently lossy; partial RangeSelections keep per-child slicing and excludeFromCopy children remain excluded.

Exports marked @deprecated in v0.32.1 (2025-06-04, >12 months ago) with no Meta-internal consumers are removed:

  • @lexical/link: toggleLink → use $toggleLink
  • @lexical/list: insertList / removeList → use $insertList / $removeList (from an update or command listener)
  • @lexical/react: the ContentEditable Props type alias → use ContentEditableProps

Note: lexical's $nodesOfType is intentionally un-deprecated (it still has many consumers). The remaining v0.32.1 deprecations that still have internal consumers (KEY_MODIFIER_COMMAND, $wrapNodes, the @lexical/table row/column helpers, etc.) are left for a follow-up.

New APIs

lexical — editor.read(mode, fn) and EditorReadMode (#8702)

read gains an optional first argument: 'force-commit' (the default; flushes pending updates first — the previous one-argument behavior), 'pending' (reads the pending state without flushing — the old editor.readPending, now removed), and 'latest' (reads the committed state without flushing, equivalent to editor.getEditorState().read(fn, {editor})). EditorReadMode is exported. readPending only shipped after v0.45.0, so its removal is not considered a breaking change.

lexical — DOM shadow root support (#8694)

An editor can now mount inside an open shadow tree without losing selection, focus, drag-and-drop, IME composition, or floating-UI behavior. Reads go through platform APIs — Selection.getComposedRanges, Selection.direction, ShadowRoot.activeElement, Document.caretPositionFromPoint(x, y, {shadowRoots}) — and nine new helpers ship from lexical; the light-DOM code paths are unchanged. Follow-ups [#8708] (insert nodes at the block cursor inside a shadow root) and [#8740] (ignore beforeinput/input in captured decorators, fixing Firefox 152) complete the surface.

lexical — onWarn editor hook (#8644)

CreateEditorArgs gains an optional onWarn?: ErrorHandler (stored as _onWarn, defaulting to console.warn), mirroring onError. The infinite-update-loop guard's recoverable warning now routes through it (wired end-to-end in [#8658]), so embedders that bridge reporting to their own telemetry no longer lose this signal to each user's browser console.

lexical — $config() accessor-based nominal typing (#8645)

An additive extension of the $config() protocol: abstract base classes can declare configuration shared with their subclasses under a Symbol.for(<ClassName>) key (resolved by getStaticNodeConfig), and accessor-based nominal typing lets subclasses opt into stricter $config() checks. No existing node class is changed.

@lexical/history — Cut/paste get their own undo entry (#8649)

Any update tagged PASTE_TAG or CUT_TAG is now classified as an OTHER change inside @lexical/history, which keeps it from merging into the preceding keystrokes and keeps following keystrokes from merging into it. Undoing a short paste (or an iOS autocorrect/prediction) or a cut no longer also undoes the text you typed immediately before it.

@lexical/extension — SelectBlockExtension (#8532)

Overrides SELECT_ALL_COMMAND so the first invocation selects the nearest block element and the second selects the entire document; a selection already spanning multiple blocks expands straight to the document, and select-all is a no-op when everything is already selected. Includes an opt-in cascadeSelection option for nested editors (image captions, etc.) and enables PreventSelectAllExtension by default to keep keydown from leaking out of input/textarea elements. (Also introduced editor.read('pending', …) via the now-generalized [#8702].)

@lexical/yjs / @lexical/react — Collaborative cursors via the CSS Custom Highlight API (#8550)

Remote collaborators' selections are now painted with the widely-supported CSS Custom Highlight API; the error-prone absolutely-positioned overlay remains only as a fallback. The remote caret is still rendered as a positioned element. Closes [#4457], [#5837].

@lexical/utils — dedupeSelectionRects (#8709)

A new exported helper that drops zero-area rects and any rect that contains another (with 1px tolerance) from Range.getClientRects(), fixing the duplicate/over-bright and spurious extra-wide fake-selection rects WebKit produces on some blocks. It is wired into positionNodeOnRange, so every fake-selection consumer (markSelection, selectionAlwaysOnDisplay, the extension) gets clean rects. Addresses [#7106], [#7492].

@lexical/code-core — escapeWithArrows (#8393)

CodeIndentExtension gains an escapeWithArrows option (default false): when a code block is the terminal node and the caret is at the end of its text, pressing the arrow keys creates a new paragraph and moves the caret into it. New $onEscapeDown / $onEscapeUp helpers back it in @lexical/utils. Closes [#7912], [#4685], [#5968].

@lexical/react — Self-contained LexicalErrorBoundary (#8720)

LexicalErrorBoundary no longer wraps react-error-boundary (the dependency is dropped) and gains an optional fallback prop — omit it for the default message, or pass fallback={null} to render nothing. Its public type is unchanged, so it remains usable as the ErrorBoundary prop of RichTextPlugin / PlainTextPlugin.

Highlights

Core (lexical):

  • ⚠️ [#8661] Deprecate unsafe type parameters on node traversal methods
  • ⚠️ [#8588] $getNearestNodeFromDOMNode(rootElement) resolves to RootNode; generalized captured selection via setDOMUnmanaged({captureSelection})
  • ⚠️ [#8615] Preserve the first linebreak in insertNodes; tag managed <br> with data-lexical-managed-linebreak
  • 🆕 [#8694] DOM shadow root support via platform selection APIs
  • 🆕 [#8702] editor.read(mode, fn) overload + EditorReadMode (replaces readPending)
  • 🆕 [#8644] onWarn editor hook (#8658 routes the recursion guard through it end-to-end)
  • 🆕 [#8645] $config() accessor-based nominal typing
  • âś… [#8598] TabNode.setTextContent for Safari IME composition
  • âś… [#8604] Caret stuck when a block has no leading or trailing text
  • âś… [#8680] Emit COMPOSITION_END_TAG from the Firefox onInput defer branch
  • âś… [#8701] Recheck text-node contents on deletion with composition
  • âś… [#8686] Reuse the empty trailing block when typing at root + last-offset selection
  • âś… [#8708] Insert nodes at the block cursor inside a shadow root
  • âś… [#8715] Normalize non-inline nodes when inserting into inline-only parents
  • âś… [#8740] Ignore beforeinput/input in captured decorators (Firefox 152)
  • âś… [#8635] [#8631] [#8612] [#8638] Stop the infinite-update-loop detector over-firing on fast typing; report via devInvariant
  • âś… [#8617] $assumeEditor warns via devInvariant instead of throwing in prod
  • âś… [#8726] Guard the klass.prototype null check in getStaticNodeConfig
  • âś… [#8739] Use an inheritsLoose-safe helper for class-inheritance loops
  • âś… [#8593] Harden against ReDoS and prototype pollution (linear-time regexes, prototype guards); faster CSS parsing
  • đź§ą [#8628] Surface a clear error when TypeScript (<5.2) can't read the package exports
  • đź§ą [#8579] Consolidate tokenization through tokenizeRawText / $generateNodesFromRawText
  • đź§ą [#8735] Better error messages for invalid node classes; cache getStaticNodeConfig
  • đź§ą [#8675] [#8742] [#8667] T[] over Array<T>, modernized Flow stubs, dropped deprecated traversal type params in tests

Named slots (experimental):

  • ⚠️🆕 [#8603] Named slots — a host node owns multiple isolated editable regions inside its own editorState
  • âś… [#8716] Named-slot typing / Backspace / Copy / hydrate paths

HTML / DOM import:

  • ⚠️ [#8662] Register DOMImportExtension rules implicitly via node extensions; make tree-shaking annotations effective
  • đź§ą [#8590] Migrate the playground's HTML import/export to the DOMImportExtension pipeline
  • âś… [#8589] $generateHtmlFromNodes self-establishes active-editor scope (back-compat for [#8519])

Extension / History:

  • 🆕 [#8532] SelectBlockExtension
  • 🆕 [#8649] Snapshot history before cut/paste so an undo doesn't swallow prior typing

Code:

  • 🆕 [#8393] escapeWithArrows: create a paragraph around a terminal code block with the arrow keys
  • 🆕 [#8600] Add Go to the code-language options
  • âś… [#8606] Set the unsupported-syntax flag only once (shiki)
  • âś… [#8513] Skip link wrapping on paste for non-simple text nodes
  • âś… [#8705] Preserve the LinkNode wrap on copy in Firefox/Safari
  • âś… [#8676] Preserve a previous DecoratorNode on Backspace at the start of a top-level list

Markdown:

  • âś… [#8688] Code spans bind tighter than text-match transformers
  • âś… [#8723] Inline code spans containing backticks
  • âś… [#8728] Preserve inline formatting when wrapping already-formatted text with matching markers
  • âś… [#8678] Update the ordered-list start when typing a marker before it
  • âś… [#8562] Preserve block equation markdown

Mark / Clipboard / Table:

  • đź§ą [#8717] Widen MarkNode method return types to boolean (restores subclass overrides)
  • âś… [#8706] Correct clipboard type-checking
  • âś… [#8674] Don't throw on stale table node keys in $handleTableSelectionChangeCommand

Collaboration (@lexical/yjs) / Dragon:

  • 🆕 [#8550] Render collab cursors via the CSS Custom Highlight API
  • âś… [#8611] Local range selection no longer grows when a collaborator edits
  • âś… [#8652] Keep the collab cursor at element end when decoding an out-of-range position
  • âś… [#8651] [#6614] Avoid empty-paragraph echo and a splice crash on collab undo; don't preserve tags on non-dirty updates
  • âś… [#8646] Fix Yjs desync after clearing all nodes
  • âś… [#8665] @lexical/dragon: handle makeChanges messages and the listener-registration race

Utils / React:

  • 🆕 [#8709] dedupeSelectionRects — clean up WebKit fake-selection rects
  • đź§ą [#8733] Move getScrollParent to @lexical/utils
  • âś… [#8684] Fix double paragraph creation when exiting a nested code block
  • đź§ą [#8720] Self-contained LexicalErrorBoundary (drops react-error-boundary) with an optional fallback prop
  • đź§ą [#8682] Hook syntax in .js.flow files; 📝 [#8714] API doc coverage of @lexical/react exports

Rich Text / Plain Text:

  • âś… [#8725] Refresh the iOS keyboard suggestion bar after Backspace for all locales
  • âś… [#8663] Call preventDefault() in dragover for HTML5 DnD compliance

Playground:

  • 🆕 [#8574] Korean IME autocomplete with composition-idle ghost text (pluggable AutocompleteDictionary)
  • 🆕 [#8594] Non-printing marks (¶, ↵, →, ·), toggled from Settings
  • âś… [#8599] Autocomplete wordlist returns the highest-priority completion
  • âś… [#8666] Clear block alignment/indent on a collapsed (but not partial) selection
  • âś… [#8719] Render PageBreakNode as <hr> so Safari fires beforeinput after paste
  • âś… [#8655] Fix the importmap of the /esm/ proof of concept
  • âś… [#8695] Correct the CodeBlock layout-exit e2e expected HTML
  • đź§ą [#8585] [#8595] [#8637] Audit and de-flake the e2e suite (remove all @flaky tags)
  • đź§ą [#8639] Remove the localhost:1235 validation-server code from ActionsPlugin

Infrastructure / Tooling:

  • đź§ą [#8587] [#8597] [#8747] Consolidate release workflows and adopt npm trusted publishing
  • đź§ą [#8614] Vitest browser-mode tests via the Playwright runner
  • đź§ą [#8673] Fix intermittent EACCES in Windows CI browser tests
  • đź§ą [#8703] Migrate editor.getEditorState().read(...) to editor.read('latest', ...)
  • đź§ą [#8634] Require all changes to be backwards compatible (AGENTS.md / CLAUDE.md)
  • đź§ą [#8693] Remove the unused tmp dependency; [#8647] replace the node-state-style example with the dev-examples version
  • đź§ą Dependency bumps: [#8566] [#8573] [#8618] [#8619] [#8621] [#8622] [#8624] [#8625] [#8626] [#8629] [#8630] [#8632] [#8633]

What's Changed

New Contributors

Full Changelog: https://github.com/facebook/lexical/compare/v0.45.0...v0.46.0

Source: README.md, updated 2026-06-25