Skip to content

Releases: meta/lexical

v0.53.0

Choose a tag to compare

@zurfyx zurfyx released this 28 Oct 15:52

v0.53.0 is the monthly release and collects a month of work since v0.52.0. It removes the extensions deprecated as of v0.52.0 and the unchecked node type arguments, starts a curated CHANGELOG.md with migration notes, and adds several opt-in, experimental building blocks for structured documents and collaboration: declarative node structure in $config, stable block ids, set-valued NodeState with a wrapper-free MarkStateExtension, and GFM tables in @lexical/mdast. Fixes cover IME composition, spellcheck, inline decorators, iOS taps and drops, tables, Markdown, and the ordering of commit listeners that collaboration depends on.

Breaking Changes

  • All packages: extensions deprecated as of v0.52.0 are removed (#9316). Migrate as follows:
    • CodeImportExtension (@lexical/code-core): use CodeExtension
    • CodeHighlighterShikiExtension, CodeHighlighterShikiConfig (@lexical/code-shiki): use CodeShikiExtension with {tokenizer}
    • HorizontalRuleImportExtension (@lexical/html): use HorizontalRuleExtension plus any extension that brings in CoreImportExtension
    • LinkImportExtension, ListImportExtension, RichTextImportExtension, TableImportExtension: use LinkExtension, ListExtension, RichTextExtension, TableExtension
    • MdastImportExtension, MdastExportExtension and their *Output types (@lexical/mdast): use MdastExtension and MdastExtensionOutput
    • The *ImportRules arrays are not extensions and remain.
  • @lexical/mdast (experimental): table column alignment is no longer stored in the mdastTableAlign NodeState. Alignment is now the element format of the paragraph in each cell, so editor states serialized with the old state lose their column alignment on load (#9478)
  • lexical: The deprecated unchecked type arguments on node traversal and key lookup are removed from TypeScript and Flow: getParent<T>(), getParentOrThrow<T>(), getChildren<T>(), getFirstChild<T>(), getNextSibling<T>(), $getNodeByKey<T>() and the rest of that family. Runtime behavior is unchanged. Remove the type argument and narrow the result with a node guard such as $isListItemNode (#9479)
  • lexical: ElementNode.reconcileObservedMutation (@internal) restores the DOM of the element's committed children from the latest reconciled state, not the children of the update it is called in. Code that called it inside an update to render that update's children must place those children's DOM itself (#9329)

New APIs & Features

  • lexical / @lexical/extension (experimental): Declarative node structure in $config. New optional keys inline, children, mergeAdjacent, and a derived flag on fields and states describe what an element may hold and when adjacent siblings are the same thing, so importers, serializers and storage layers can read rules that used to live only in transforms. getDeclaredNodeStructure(klass) reads them, and the opt-in NodeStructureExtension enforces them (#9452)
  • @lexical/extension / @lexical/html (experimental): Stable block ids. BlockIdExtension gives blocks an id that survives serialization, reloads, undo/redo, the clipboard and collaboration, stored as NodeState ("$": {"blockId": "…"}), so agents and server pipelines can address blocks directly. BlockIdHTMLExtension, imported from @lexical/html/BlockIdHTMLExtension, carries the ids through HTML (#9421, #9474)
  • lexical / @lexical/mark (experimental): Set-valued NodeState and MarkStateExtension. arraySetValue(item) declares a state whose value is a canonical (sorted, deduplicated, frozen) set of strings. MarkStateExtension uses it to store mark ids on the leaves they cover instead of in MarkNode wrappers, so overlapping comments need no tree restructuring and ids travel with the text (#9380)
  • @lexical/mdast (experimental): GFM tables. MdastTableExtension imports tables the way GFM renders them and exports any table as a GFM table, using <br> for multi-block cells and padding merged or ragged cells (#9478)
  • @lexical/clipboard: Clipboard import middleware can replace the data and selection for the remaining handlers with $next(data?, selection?), the equivalent of ProseMirror's transformPastedHTML, so off-the-shelf HTML cleanup can be plugged in (#9312)
  • @lexical/link: ClickableLinkExtension accepts getWindow(editor), so iframe editors can navigate in their own window or leave the click unhandled (#9360)
  • @lexical/react: LexicalAutoEmbedPlugin accepts $disableMenuFor to suppress the embed menu where a block embed does not belong, such as inline-only slots (#9365)
  • Changelog with migration notes: CHANGELOG.md is now built from changesets, with Breaking changes (each with Migration: and Detect:), Behavior changes, Deprecations and the full commit list for every release, and it ships in the lexical package at node_modules/lexical/CHANGELOG.md for people and tools upgrading across versions (#9357)
  • Continuous builds: every PR and main commit is published to pkg.pr.new, so a fix can be tried before the next release with npm i https://pkg.pr.new/lexical@main (npm only; see the PR for pnpm and yarn) (#9385, #9387, #9394)

Notable Fixes

Security

  • Links opened in a new tab by registerClickableLink / ClickableLinkPlugin no longer get window.opener, closing a reverse-tabnabbing path (#9303)
  • Markdown export escapes literal brackets and significant < and &, so literal text no longer turns into HTML, character references or links when the Markdown is parsed again. Import decodes escapes and character references in a single pass (#9481)

Input, composition & spellcheck

  • Composing into a TextNode subclass with an IME or a dead key no longer replaces it with a plain TextNode, so it keeps its type, key and NodeState (#9291); cancelled macOS Pinyin compositions in desktop WebKit finish, so the next Enter works (#9362)
  • Chromium keeps spellcheck underlines when Lexical rewrites a text node (#9354); Markdown shortcuts work in iframe editors (#9352); selectionchange events from unfocused inputs and textareas no longer clear pending formatting (#9477); copy, cut and paste inside a decorator's own input are left to that input (#9288)

Selection & editing

  • Mouse drags select across inline decorators such as images, equations and mentions (#9329); line deletion keeps the line break before a line starting with an inline decorator and preserves decorators across soft wraps (#9301, #9321)
  • Selected text and formatting are preserved at range boundaries, and $patchStyleText now styles the selection it is given (#9261); caret mutations, endpoint repair and cross-parent moves no longer corrupt sibling links, throw, or misorder the DOM (#9262, #9263, #9302)
  • Selections and deletions stay inside slot boundaries (#9373); root text content stays correct after repeated slot edits and for custom getTextContent() overrides (#9283, #9260)
  • Enter splits a heading correctly after a line break or inline decorator (#9295); removing a list keeps a caret that sits after an inline decorator (#9300); HISTORY_PUSH_TAG always pushes its own undo entry (#9218); setting a NodeState key still held in unknownState no longer leaves the stale JSON value behind (#9468)

iOS

  • Tapping a decorator no longer ...
Read more

v0.52.0

Choose a tag to compare

@etrepum etrepum released this 28 Sep 15:31

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_COMMAND consistently runs before DOM reconciliation, inside the pending writable update. This includes automatic NodeSelection and TableSelection notifications 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 native selectionchange event. 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 a COLLABORATION_TAG or HISTORIC_TAG update 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. Use registerUpdateListener for UI that must respond to every commit, including content-only changes (#9219, selection timing guide)
  • lexical / @lexical/html — Default HTML export now honors DOMRenderExtension $createDOM overrides. This affects nodes that inherit LexicalNode.exportDOM or call super.exportDOM(editor), including text and paragraphs. If an override was intended only for the live editor, review the HTML it now exports. Custom exportDOM implementations that create their own elements can still bypass this hook; $updateDOM and $decorateDOM remain reconciliation-only, and $exportDOM handles export-specific changes (#9231)
  • lexical — LexicalNode.getCommonAncestor is 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 — MdastImportExtension and MdastExportExtension are deprecated aliases of MdastExtension. Move configuration to MdastExtension and replace MdastImportExtensionOutput / MdastExportExtensionOutput with MdastExtensionOutput. 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. MdastExtension owns 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. A TextNode rule can therefore handle tabs and custom text subclasses without separate registrations (#9198)
  • @lexical/mdast — Import/export middleware through context.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. Use context.next() to delegate: null retains 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 — HeadlessExtension works with buildEditorFromExtensions for server-side processing and tests. It marks the editor as headless and prevents attaching a DOM root. Unlike createHeadlessEditor, 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)
  • setEditorState with a childless root warns and recovers to an empty paragraph in production. Development builds still report the invalid state through onError (#9183)

Performance & signals

  • Writable nodes are cached during pending updates, reducing repeated node-map lookups (#9169)
  • @lexical/extension raises its @preact/signals-core dependency 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 $config schemas, 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...
Read more

v0.51.0

Choose a tag to compare

@etrepum etrepum released this 17 Sep 21:05

v0.51.0 is a monthly release with one large new feature: nodes can now declare their JSON serialization as a schema on $config. This declarative schema is faster, safer, and more flexible than the hand-written methods. The metadata it provides enables a compact export option with all default values elided, the code generator that provides faster import and export for every node that lexical ships, a new @lexical/fast-check package for property-based tests, and it provides a path to future features that rely on knowing the structure of the nodes (e.g. additional codecs and dev tools).

This is a big step towards Lexical 1.0, the next few releases are expected to be focused on moving everything (including docs and examples) to the latest Extension and $config APIs and removing deprecated functionality. Two related changes in this release are that we are publishing ESM-only to npm now (cjs builds are still supported in the source tree), and LexicalComposer is deprecated for LexicalExtensionComposer. Expect anything @deprecated to be removed soon.

Breaking Changes

  • npm packages are ESM only. Every package declares "type": "module" and its exports map offers development / production / default conditions with no require branch; the CommonJS build remains in-tree only for Meta's www. Bundlers are unaffected. CommonJS code can require() the packages on Node.js 20.19+ (the builds have no top-level await), and older Node.js must use await import(...) — but a dependency the app also loads through require() can then exist twice, which breaks handing its objects to Lexical (yjs docs to @lexical/yjs, @preact/signals-core signals from @lexical/extension), so load those as ESM too. Metro bundles both builds unless you add the condition for Lexical's packages; see the module formats FAQ (#9127)
  • lexical — exportJSON may serialize the instance as-is. A property declared with withField is read straight off the node instead of through its accessor, so exportJSON no longer resolves getLatest() for you and a stale reference can write stale values (and a property whose accessor a subclass overrode still resolves, so one object can mix the two). Nothing inside Lexical is affected — the walk, the clipboard selection export and editorState.toJSON() all start from the node map — but call getLatest() yourself on a reference you kept across a mutation (#8602)
  • @lexical/code — the deprecated Prism re-exports are gone, along with the @lexical/code-prism dependency (so Prism no longer lands in a bundle that only imports @lexical/code). Import CODE_LANGUAGE_FRIENDLY_NAME_MAP, CODE_LANGUAGE_MAP, getCodeLanguageOptions, getCodeLanguages, getCodeThemeOptions, getLanguageFriendlyName, normalizeCodeLanguage (formerly also normalizeCodeLang), PrismTokenizer and registerCodeHighlighting from @lexical/code-prism (#9141)

Deprecations

  • @lexical/react — LexicalComposer is deprecated in favor of LexicalExtensionComposer and is expected to be removed in a future major. LexicalComposer cannot accept extensions, so any feature shipped as one is unreachable from an editor it builds; the migration is usually a two-line change (#9168, React extensions guide)

New APIs & Features

  • lexical — Declarative serialization schemas (experimental). A node that uses $config declares its serialized properties once in the new json property, and that single declaration drives importJSON, updateFromJSON and exportJSON: nodeSchema, withField / withAccessors, and the value combinators (stringValue, numberValue, booleanValue, enumValue, arrayValue, objectValue, unionValue, rawValue, transformValue, nullable, optional, …). Every built-in node declares one, and most custom nodes now need no JSON serialization code at all. Output is unchanged by default (#8602, serialization docs).
  • lexical — Compact JSON export, opt-in via editorState.toJSON(true) or $withCompactExport(true, cb) (with $isCompactExport() for schema getters, plus $exportNodeJSON and the CompactSerializedEditorState / ParsableSerializedEditorState / LexicalParseJSON / SerializedPartial types). It omits properties equal to their schema default, properties the parser derives, and the deprecated version. The raw objects are well under half the legacy size — which matters for structured clones and worker messages — but after gzip the two are a wash. Both forms parse back to the same document; only a Lexical new enough to know the schemas can read the compact one, so keep writing the legacy form until every reader is upgraded (#8602)
  • @lexical/compiler — Generated serialization code. Every built-in node class ships straight-line serialization generated from its own schema at build time, producing byte-identical JSON to the schema-driven path that continues to serve custom nodes (#8602)
  • @lexical/fast-check — New package deriving fast-check arbitraries from those schemas: nodeArbitrary(NodeClass) generates exactly the serialized JSON a node's parser accepts, each property independently present or absent. Useful for the bugs fixtures hide, such as a property afterCloneFrom forgot to copy. fast-check is a peer dependency; getComposedSchemaFields in core inspects a class's schema without generating anything (#8602)
  • @lexical/extension — Published as subpath exports, one entry per extension (@lexical/extension/AutoFocusExtension, …), with compiler and lint enforcement of the source import convention. The barrel import is unchanged, and shared modules such as signals keep a single instance (#9161). This is primarily only useful as a consumer if you are not using tree-shaking.

Notable Fixes

Bundling, tree-shaking & types

  • A production import of only createCommand no longer retains the editor (#9121), and the remaining module-scope side effects that defeated tree-shaking are gone, with a bare-import residue check now run over every entry (#9124); extension editors build under loose-mode transpilation (#9134)
  • Flow: defineExtension was uncallable, making the extension types unusable (#9132); declarations can now express an extension with dependencies (#9176); modern variance keywords survive into the www stubs (#9154)

Selection & editing

  • No redundant selection-change command on Safari link selection (#9163); TextNode.setMode, setTextContent and isSimpleText read the latest state (#9162); $removeTextFromCaretRange no longer leaves a childless list or table as the root's only child (#9116); a text entity keeps the style and detail of the text it replaces (#9130); selection overlay listeners are notified when rectangles are removed (#9117)

Lists, tables & Markdown

  • Indenting between two nested lists no longer converts the second list's type (#9118); $mergeCells no longer keeps the empty paragraph of the target cell (#9122); text left outside a Markdown link keeps its format (#9131)

Playground & docs

  • Card/Review CSS placeholders are cleared reliably on paste in Safari (#9129) and releasing a CardNode drag no longer selects the node (#9135); new docs on Lexical being a singleton per app, and why a library must declare it as a peer dependency (#9125)

Plus the usual dependency updates across the rollup, shikijs, vitest, docusaurus/typedoc and flow/hermes groups.

What's Changed

  • v0.50.0 by @etrepum in #9110
  • [lexical][lexical-compiler] Bug Fix: Let esbuild drop the editor from a createCommand-only production import by @zurfyx in #9121
  • [*] Bug Fix: Remove the remaining module-scope side effects that defeat tree-shaking by @etrepum in #9124
  • [lexical-playground] Bug Fix: clear Card/Review CSS placeholders reliably on paste in Safari by @sah...
Read more

v0.50.0

Choose a tag to compare

@etrepum etrepum released this 02 Sep 22:18

v0.50.0 is a monthly release with a large number of PRs to improve correctness, accessibility, and performance. It also ships HMR support, compiled keyboard shortcut dispatch, and screen reader announcements for headings and auto-links, and build-time tree-shaking annotations (in a new @lexical/compiler package). We have upgraded to pnpm 11, so if you're working directly with the monorepo you will need to upgrade as well.

Breaking Changes

  • lexical — LexicalNode.replace() no longer re-homes a named slot value. A slot value has no parent (its up-link is __slotHost), so replacing one through the tree API now fails with an actionable invariant instead of a generic parent error; use $setSlot on the owning host. $setBlocksType and the block converters skip slot values for the same reason (#8938, #8905, #8901)
  • @lexical/clipboard — The Lexical JSON clipboard payload (application/x-lexical-editor) now asks excludeFromCopy('clone') instead of 'html', matching the destination the rest of that payload already uses. Nodes that opt out of HTML export but expect to survive a clone (e.g. MarkNode) are copied again; custom excludeFromCopy implementations that keyed on 'html' for this channel should be updated (#8995)

Behavior Changes to Watch

These were merged as bug fixes rather than breaking changes, but they change observable output — check them if you snapshot HTML, assert on the DOM, or depend on the old editing behavior.

  • HTML export emits a terminating <br>. A trailing LineBreakNode in a block now exports an extra <br data-lexical-managed-linebreak="true">, mirroring what the reconciler already puts in the live DOM, so <p>a<br></p> survives an export/import round trip. Both importers drop the marker, but exported-HTML fixtures will differ (#8765)
  • Rich text mounts an aria-live region by default. HeadingAnnounceExtension (via RichTextExtension) and AutoLinkAnnounceExtension (via AutoLinkExtension) depend on AriaLiveRegionExtension, which appends a live-region <div> to document.body — or to the configured owner element. Opt out with configExtension(HeadingAnnounceExtension, {disabled: true}) (#8908, #9071)
  • Select-all delete and cut now remove the blocks. A whole-document range is widened to the top-level blocks, so the editor is left on a fresh empty paragraph instead of an emptied heading, quote or list that went on styling the next character typed — and Cmd+X now carries those blocks, so pasting restores them (#8884)
  • A formatted paragraph copies as a block. ParagraphNode.extractWithChild now keeps a fully-selected paragraph that carries alignment, indent or inline style in the payload as a block, instead of splicing its children in and dropping that formatting. Paragraphs with none of it serialize as before (#9059)
  • importJSON applies registered node replacements. JSON deserialization builds through $create, so a replacement's class is constructed and its importJSON runs; a deprecated replace without withKlass is still applied the old way (#9061, #9090)
  • Listener return types tightened. Only RootListener and EditableListener are typed to return a cleanup function; returning one from another listener is now a TypeScript error — it was never called (#8990)
  • Collab and Markdown output shifts. @lexical/yjs no longer syncs root textFormat/textStyle (#8940); Markdown import restructures nested mixed-type lists and stores fence metadata in a new mdCodeMeta node state (#8283)
  • New invariants. Replacing a slot value (#8938), an insertText anchor that is not a text node, an empty KeyboardShortcutMatch key, and a createBindingV2 getXmlText/getXmlElement that returns an un-integrated or misnamed type (#6483) now throw instead of failing later

Deprecations

  • lexical — CAN_UNDO_COMMAND and CAN_REDO_COMMAND are deprecated in favor of the canUndo / canRedo signals on HistoryExtension. A command only reports a change, so a listener registered after initialization can't read the current value; the signal always holds it (#8918)

New APIs & Features

  • @lexical/compiler — New package that injects /* @__PURE__ */ annotations and inlines the trivial side-effect-free factories (defineExtension, configExtension, safeCast, createCommand, createState, …) at build time, as a Rollup and Vite plugin. Hand-written annotations on those calls are no longer needed (#9086)
  • @lexical/extension — HMRExtension (with HMRConfig, HMROutput, HotContext) keeps an editor alive across hot module replacement. @lexical/react gains LexicalMenuOption, LexicalAutoEmbedPluginUtils, LexicalTypeaheadMenuPluginUtils and LexicalCollaborationContextUtils entry points that split values out of component modules so Fast Refresh works — the original exports are unchanged (#8959)
  • lexical / @lexical/extension — Compiled keyboard shortcut dispatch: compileKeyboardShortcuts, CONTROL_OR_ALT, CONTROL_OR_META, CONTROL_OR_OTHER_KEY, keyboardEventMaskForPlatform and the KeyboardShortcut* types in core, plus KeyboardShortcutsExtension and formatKeyboardShortcut for declaring named shortcuts and rendering them per platform (#8876)
  • @lexical/rich-text — HeadingAnnounceExtension announces heading changes to screen readers, on by default in RichTextExtension; @lexical/link's AutoLinkAnnounceExtension does the same when typing turns text into a link, on by default with AutoLinkExtension (#8908, #9071)
  • @lexical/list — Opt-in WordListImportExtension reconstructs list structure from Microsoft Word HTML (MsoListParagraph classes and mso-list metadata) on paste (#9082)
  • @lexical/rich-text — New shouldHandlePasteAsFiles config (and exported defaultShouldHandlePasteAsFiles, plus an optional third argument to registerRichText) lets an app decide that pasted files win over the incidental text/html browsers attach when an image is copied. The default is unchanged: files are handled only when the clipboard carries no text content (#8902, #8699)
  • @lexical/yjs — CreateBindingV2Options__EXPERIMENTAL allows a custom Yjs XmlText, so a Lexical document can live inside a larger shared type (#6483)
  • lexical — New $getSelectionSlotFrame (#9052) and $flushSyncAfterUpdate (#9067) exports; $isBlockFullySelected moved into core and is still re-exported from @lexical/utils (#9059); @lexical/table now exports the TableCellHeaderState type (#9038); JSDoc added across the core exports (#8895)
  • Performance — Bulk inserts and removals no longer walk the parent's children to compute an offset the selection never reads, which made those operations quadratic (#9108)

Notable Fixes

Named slots & the node tree

  • Line breaks inside a slot insert and delete correctly (#8899); a managed line break is restored in wrapped slot hosts (#9066); reading the parent style of a slot value no longer throws (#8950); a slot-bearing decorator host is deleted correctly at the selection boundary (#8905)
  • replace() keeps the parent's __size correct when handed a sibling (#8917), and splice no longer creates a sibling cycle when it re-ins...
Read more

v0.49.0

Choose a tag to compare

@etrepum etrepum released this 30 Jul 16:18

v0.49.0 is a monthly release headlined by the completion of the $config() protocol migration for Lexical's built-in node classes (#8640), a breaking change that replaces per-node getType()/clone()/importJSON()/importDOM()/transform() boilerplate with runtime-synthesized behavior. It ships with a cluster of follow-up fixes hardening the synthesized getType() and clone() (including a stack overflow in compiled builds), an opt-in sticky horizontal scrollbar for overflowing tables, and a broad batch of fixes across tables, Markdown, HTML, code highlighting, and selection.

Breaking Changes

  • lexical — Built-in node classes are ported to the $config() protocol. The static importJSON(), importDOM(), clone(), and transform() methods are no longer present on ported nodes; use the higher-level equivalents instead (LexicalEditor.parseEditorState/$generateNodesFromDOM, the $cloneWithProperties helper, and extensions or $transform in $config). getType() is unchanged and safe to keep using, __type is now readonly, and node constructors require zero-argument defaults. Custom nodes may keep their static methods, but adopting $config() is recommended (#8640)
  • lexical — LexicalCommand<T>'s payload type is now invariant to prevent unsafe structural compatibility between commands. Previously correct code (including explicit generics) needs no changes; only unsafe LexicalCommand<unknown> usage must switch to the newly exported AnyLexicalCommand alias. dispatchCommand's payload argument is now optional when a command's payload type is undefined or void, and redundant explicit type parameters were dropped from the registerCommand/registerNodeTransform call sites (#8877)

New APIs & Features

  • @lexical/table — Opt-in sticky horizontal scrollbar for tables wider than their container, so the scrollbar stays anchored at the viewport bottom instead of only being reachable at the end of the table. Enable it with hasStickyScrollbar: true in the table config; style it via the new tableStickyScrollbar theme key (#8790)
  • @lexical/table — The <colgroup> element is now omitted from the table DOM when column widths are undefined, producing cleaner markup and letting the browser auto-size columns; it is created or removed dynamically as widths change (#8850)
  • lexical — Added an editor operation benchmark suite (paragraph split, bold formatting, range deletion, paste, select-all format) measured through full DOM reconciliation, to help contributors track core editing performance (#8856)

Deprecations

  • @lexical/table — $createTableSelection is deprecated in favor of $createTableSelectionFrom, which takes the table node plus anchor/focus cells and validates them, instead of returning a blank selection with placeholder 'root' keys that must be overwritten (#8855)
  • @lexical/table — getShape() is deprecated because it computes incorrect bounds for merged cells; use the now-exported $computeTableCellRectBoundary (with $computeTableMap) instead (#8853)

Notable Fixes

$config() follow-ups (#8640)

  • The synthesized clone() now applies afterCloneFrom() when called directly (e.g. NodeClass.clone(node)), fixing silent property loss across 25+ core nodes (#8864)
  • The synthesized getType() no longer returns the superclass type when inherited, fixing node-type collisions during editor registration (#8867)
  • The synthesized getType() no longer recurses infinitely when a bundler copies it onto a subclass as an own static, fixing stack overflows in compiled/minified builds (#8869)

Tables

  • DELETE_LINE_COMMAND (Cmd/Ctrl-based delete-line shortcuts) now works inside table cells instead of being silently swallowed (#8851)
  • Table alignment now works when cells are selected in any direction, not just top-left→bottom-right (#8883)

Selection & editing

  • Editor updates dispatched from a read-only context (e.g. inside editor.read()) now run in a fresh writable update instead of being silently dropped, with a dev warning (#8863)
  • Firefox now creates a selection and shows the block cursor when clicking in the gap between block decorators (#8862)
  • Pressing ArrowUp before a leading non-inline decorator no longer moves the selection out of the editor (#8887)

Markdown, HTML & code

  • Typing a list marker at the start of a heading no longer converts the heading into a list (#8879)
  • HTML import now evaluates unrestricted CSS selector groups (e.g. p, .foo) against all elements instead of only tag-matched ones (#8873)
  • Concurrent async language/theme loads in the Shiki highlighter are deduplicated and merged into a single history entry, avoiding spurious undo states (#8854)
  • QuoteNode.updateDOM now accepts the EditorConfig argument the reconciler passes to every other node, for API consistency (#8882)

Playground & website

  • Playground scroll padding now accounts for the sticky toolbar so selections scrolled into view from above aren't hidden behind it (#8849)
  • Fixed a homepage crash caused by a minified dev build, and isolated each embedded example in its own error boundary so one failure no longer takes down the page (#8861)

What's Changed

  • [lexical] Bug Fix: Scope bench vitest projects to exclude regular test files by @mayrang in #8852
  • [lexical-table] Bug Fix: Enable DELETE_LINE_COMMAND in table cells by @mayrang in #8851
  • [lexical-table] Chore: Deprecate $createTableSelection in favor of $createTableSelectionFrom by @mayrang in #8855
  • [Breaking Changes][lexical] Refactor: Port node classes to the $config() protocol by @etrepum in #8640
  • [lexical-playground] Bug Fix: account for sticky toolbar in scroll padding by @etrepum in #8849
  • [lexical-list] Chore: Rename isNestedListNode to $isNestedListNode by @etrepum in #8843
  • [lexical-table][lexical-playground] Chore: Deprecate getShape() and migrate playground by @mayrang in #8853
  • [lexical-table] Chore: Remove dead code in table command handlers by @mayrang in #8857
  • [lexical-code-shiki] Bug Fix: Deduplicate async loads and merge into history by @Alxs39 in #8854
  • [lexical-table][lexical] Feature: Sticky scrollbar for overflowing tables by @mayrang in #8790
  • fix: $config() auto-synthesized clone() loses properties when called directly (#8640) by @potatowagon in #8864
  • fix: $config() synthesized getType() inherited by subclasses causes node-type collision (#8640) by @potatowagon in #8867
  • [lexical] Chore: Migrate LexicalSelection tests to buildEditorFromExtensions by @mayrang in #8865
  • [lexical] Feature: Editor operation benchmarks by @mayrang in #8856
  • [lexical] Bug Fix: run editor updates dispatched from a read-only context in a fresh writable update by @potatowagon in #8863
  • [lexical][lexical-website] Bug Fix: Homepage crash from optimized dev build by @etrepum in #8861
  • fix: $config() synthesized getType() recurses infinitely when inherited as own static (#8867 follow-up) by @potatowagon in [#8869](https://github.com/facebook/l...
Read more

v0.48.0

Choose a tag to compare

@etrepum etrepum released this 16 Jul 19:16

v0.48.0 is a maintenance release focused on bug fixes across Markdown, tables, lists, links, and selection. It's headlined by a fix for a v0.46.0 regression that broke native text drag-and-drop (#8842) and a couple of notable security hardening fixes. It also adds a handful of new features, including an MdastHtmlExtension with examples for authoring custom Markdown constructs (collapsibles, kbd, alerts, footnotes), a customizable Yjs shared-type root name for collaborative editing, and new table row manipulation helpers.

New APIs & Features

Notable Fixes

Drag & drop (fix for v0.46.0 regression)

  • Don't cancel dragover for text drags, so native drops work again (#8842)

Security

  • LinkNode.sanitizeUrl() now fails closed on unparseable URLs, preventing a potential XSS vector (#8846)
  • Fixed a serialize-javascript dependency vulnerability (#8803)

Markdown & code

  • Roundtrip overlapping inline formats correctly through mdast/Markdown (#8825)
  • Force re-tokenization after an async language load so highlighting appears once the grammar is ready (#8830)

Tables

  • Auto-scroll while drag-selecting cells past the visible edge (#8822)
  • Enable table copy in read-only mode (#8845)

Lists & character limit

  • Backspace at the start of a list item now outdents or converts to a paragraph (#8829)
  • Merge adjacent OverflowNodes in useCharacterLimit (#8831)
  • Count block separators when wrapping character-limit overflow (#8840)

Links & selection

  • Disable link opening for disabled autolinks (#8839)
  • Skip scrollIntoViewIfNeeded when the selection rect is above the editor, fixing a Safari RTL caret jump (#8848)

What's Changed

  • [lexical-mdast][lexical-markdown] Bug Fix: Roundtrip overlapping inline formats by @etrepum in #8825
  • [lexical-table][lexical-playground] Bug Fix: Auto-scroll while drag-selecting cells past the visible edge by @JohnJunior in #8822
  • [lexical-code-shiki] Bug Fix: force re-tokenize after async language load by @ochevallier in #8830
  • [lexical-react] Bug Fix: Merge adjacent OverflowNodes in useCharacterLimit by @mayrang in #8831
  • Open playground links in a new tab by @potatowagon in #8837
  • [lexical-rich-text][lexical-plain-text] Bug Fix: don't cancel dragover for text drags so native drops work again by @etrepum in #8842
  • [lexical-link] Bug Fix: disable link opening for disabled autolink in… by @ochevallier in #8839
  • [lexical-table] Feature: Add $moveTableRow function & Add missing export for $unmergeCellNode by @hamo-o in #8833
  • [lexical-list] Bug Fix: Backspace at start of list item outdents or converts to paragraph by @mayrang in #8829
  • [lexical-yjs][lexical-react] Feature: Customizable Yjs shared-type root name by @mayrang in #8841
  • [lexical] Chore: Fix serialize-javascript package dependency vulnerability by @vijayojha89 in #8803
  • [lexical-react] Bug Fix: Count block separators in character limit overflow wrapping by @mayrang in #8840
  • Fix: fail closed in LinkNode.sanitizeUrl() on unparseable URLs (XSS) by @xiezhenjia-meta in #8846
  • [lexical-extension][lexical-mdast][dev-mdast-editor-example] Feature: Add MdastHtmlExtension and Markdown custom-construct examples (collapsible, kbd, alerts, footnotes) by @etrepum in #8826
  • [lexical-react][lexical-table] Bug Fix: Enable table copy in read-only mode by @mayrang in #8845
  • Skip scrollIntoViewIfNeeded when the selection rect is above the editor (Safari RTL caret jump) by @jeremy in #8848

New Contributors

Full Changelog: v0.47.0...v0.48.0

v0.47.0

Choose a tag to compare

@etrepum etrepum released this 09 Jul 22:07

v0.47.0 is a monthly release with a lot of bug fixes, particularly related to IME and composition, and is headlined by two new experimental packages:

  • @lexical/mdast (#8794) — a micromark/mdast-based alternative to @lexical/markdown. Parsing, serialization, and markdown shortcuts all go through the same parser used by remark, so CommonMark + GFM compliance comes for free and there is no second grammar to keep in sync. The original syntax of each construct is preserved on the nodes, so re-serializing produces minimally different Markdown. @lexical/markdown remains the supported default for production apps. See the new Markdown with @lexical/mdast guide.
  • @lexical/a11y (#8591) — framework-agnostic accessibility helpers (ARIA live regions, focus management, roving tab index, focus trap) usable from React, Svelte, Vue, Solid, or vanilla DOM, with React wrappers shipping from @lexical/react. The playground adopts them as part of a WCAG AA reference pass, and the keyboard contracts Lexical follows are documented in the new Keyboard Accessibility guide.

Beyond the headliners: React 18 is now the baseline (see Breaking Changes), the playground gains Find and Replace and a Ruby annotation node with a floating editor, tables learn to spread pasted TSV across cells and get row/column header utilities, and there is a long list of IME/composition, selection, and navigation fixes.

Special recognition once again goes to @mayrang who has been plowing through IME, accessibility, and other input related issues as well as the really cool new Find and Replace and Ruby Annotations playground features.

Breaking Changes

@lexical/react / @lexical/devtools-core — React 17 support dropped (#8782)

React 18 is now the minimum supported version; the react / react-dom peerDependencies require >=18, and React 17 compatibility code paths were removed. If you are already on React 18 or 19, no changes are required.

lexical — DOM element preserved when composing on a segmented TextNode (#8784)

Starting IME composition in the middle of a segmented TextNode (e.g. a mention) previously replaced the node — destroying its DOM element and breaking the browser's composition tracking (caret flashing and styling bleed on Chrome). The node now temporarily switches to normal mode for the duration of composition, keeping the same node key and DOM element, and is cleaned up when composition ends. Subclass transforms or method overrides that assume a segmented node is always in segmented mode must account for this transient state — see the updated TextNode.setMode() documentation.

New APIs

  • lexical — $getDocument() returns the owner document of the active editor (correct inside a Shadow DOM or iframe), falling back to the global document when there is no active editor; a new @lexical/eslint-plugin rule enforces its use over the document global (#8788, #8813)
  • lexical — registerEventListener / registerEventListeners, type-safe addEventListener helpers that return cleanup functions, now used throughout the codebase — see the updated Working with DOM Events guide (#8767)
  • lexical — SET_TEXT_FORMAT_COMMAND sets (rather than toggles) the selection's text format (see Commands); the same PR fixes formatText toggle direction (#8807)
  • @lexical/markdown — $generateNodesFromMarkdownString parses a markdown string into Lexical nodes without inserting them into the document or modifying the selection (#8789)
  • @lexical/table — $setTableRowIsHeader and $setTableColumnIsHeader utilities (#8815); $insertTableRowAtNode and $insertTableColumnAtNode are now exported (#8791)

Notable Fixes

IME, composition & mobile

  • Prevent text duplication on iOS Safari when formatting during composition (#8755)
  • Skip ZWSP insertion for format mismatch on Android Chrome (#8769); deduplicate speech-to-text insertion on Android Chrome (#8759)
  • Event module globals moved into per-editor InputState (#8809); refactored IME composition test infrastructure with browser-level coverage (#8793)

Selection & navigation

  • ArrowUp/Down no longer skip block decorators (#8775); deleteLine no longer removes adjacent block decorators (#8744)
  • Keep selection inside single-child inline elements (#8772); fix navigation across unmergeable TextNode boundaries in inline-grid containers (#8797)
  • $setBlocksType properly handles block-end focus in backward selections (#8753); insertNodes re-resolves a detached firstBlock (#8764)
  • Place the block cursor between decorators and shadow roots (#8758); clean up the trailing shadow root after select-all delete (#8751)
  • deleteCharacter no longer overwrites the X11 PRIMARY selection via Selection.modify (#8774)
  • Defer onUpdate callbacks during nested commits (#8672)

Markdown, code & tables

  • Fence-like lines inside a code block stay content (#8734); prevent formatting on TabNode inside code blocks (#8752)
  • Pasted TSV text spreads across table cells (#8780)

Playground & website

  • Find and Replace (#8779) and Ruby annotation node with floating editor (#8741)
  • Support Unicode URLs in the autolink matcher (#8787); restore RTL checkbox position in checklists (#8783); forced-colors (high contrast) icon fixes (#8781, #8773)
  • Rewritten testing guide on the website (#8811)

What's Changed

  • [lexical][lexical-rich-text][lexical-selection] Bug Fix: Fix navigation across unmergeable TextNode boundaries in inline-grid containers (#8797) by @mayrang
  • [lexical][lexical-rich-text] Bug Fix: Fix formatText toggle direction and add SET_TEXT_FORMAT_COMMAND (#8807) by @mayrang
  • [scripts] Bug Fix: Let npm prompt for OTP when publishing bootstrap stubs (#8820) by @etrepum
  • [lexical-playground] Bug Fix: Clear inline font-size when converting to heading (#8800) by @mayrang
  • [lexical-table][lexical-playground] Feature: $setTableRowIsHeader and $setTableColumnIsHeader utilities (#8815) by @mayrang
  • [lexical-website] Documentation Update: Rewrite testing guide (#8811) by @mayrang
  • [lexical-mdast][lexical-rich-text] Feature: @lexical/mdast, a micromark/mdast-based alternative to @lexical/markdown (#8794) by @etrepum
  • Make dependency-check resilient to transient registry errors (#8818) by @zurfyx
  • [lexical] Refactor: Move event module globals into per-editor InputState (#8809) by @mayrang
  • [lexical] Bug Fix: $getDocument() should fall back to the global document when there is no active editor (#8813) by @potatowagon
  • [lexical-playground] Bug Fix: Keep cell background color modal open on first click (#8806) by @sahiee-dev
  • [lexical-playground] Bug Fix: Use consistent default maxWidth for markdown-imported images (#8810) by @mayrang
  • [*][lexical-devtools][lexical-playground] Chor...
Read more

v0.46.0

Choose a tag to compare

@etrepum etrepum released this 26 Jun 04:56

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:

// 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.

@lexical/link / @lexical/list / @lexical/react — Removed exports deprecated since v0.32.1 (#8704)

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 expo...

Read more

v0.45.0

Choose a tag to compare

@etrepum etrepum released this 28 May 21:49

v0.45.0 is a monthly release with several breaking changes around selection and reconcile semantics, a major new experimental DOMImportExtension API for composable HTML import that replaces importDOM, a generalized DOMSlot and DOM render override surface, new HistoryExtension ergonomics (canUndo/canRedo signals, maxDepth), notable reconciler/selection performance work, and a wide range of bug fixes across IME, mobile keyboards, list/code/table editing, and markdown round-tripping.

Breaking Changes

lexical — Shadow root inclusion in $getReconciledDirection (#8479)

$getReconciledDirection now walks through shadow roots when resolving the reconciled dir attribute on a node, so nested ShadowRoot-style elements (e.g. tables) inherit direction consistently with regular elements. Callers that relied on direction inheritance stopping at shadow roots will need to update.

lexical / @lexical/rich-text — Backspace at block start preserves the current block (#8493)

Pressing Backspace at the start of a non-empty block previously merged the current block into the previous block, which discarded the current block's type/format. The new behavior merges the previous block's children into the current block instead, preserving the current block's type (heading, list item, quote, etc.). A new $mergeBlockBackward helper on RangeSelection is exposed for custom command handlers that want the same behavior.

lexical — Selection adjustment in removeFromParent callers (#8501)

LexicalNode.replace, insertBefore, insertAfter, and related callers that move a node between parents now correctly adjust the current selection to follow the moved node instead of leaving the selection pointing at the (now empty) original slot. This may change selection state observed by transforms or update listeners that previously relied on the stale offsets.

@lexical/markdown — Apply markdown shortcuts on composition-committed triggers (#8503)

Markdown shortcuts (e.g. *, _, ~) typed via IME composition now trigger after the composition is committed, not just on raw beforeinput. This fixes shortcuts being silently dropped during Japanese / Korean / Chinese input but may also fire shortcuts in some flows that previously didn't.

@lexical/extension / @lexical/rich-text / @lexical/plain-text — NormalizeInlineElementsExtension (#8497)

registerRichText and registerPlainText now register a transform that removes empty inline elements (e.g. LinkNode, formatted TextNode wrappers with no children) instead of leaving them in the tree. The transform is also exported as NormalizeInlineElementsExtension from @lexical/extension for use with extension-based setups. Editors that intentionally kept empty inline wrappers will need to disable the extension or override the transform. See Included Extensions.

lexical / @lexical/extension / @lexical/rich-text / @lexical/plain-text — NormalizeTripleClickSelectionExtension (#8520)

The "triple-click selects the whole paragraph including the trailing newline" normalization has moved out of LexicalEvents and into a new NormalizeTripleClickSelectionExtension registered by both rich-text and plain-text. The core LexicalEditor no longer performs this normalization on its own, so editors that bypass registerRichText / registerPlainText (or the equivalent extensions) need to register the extension explicitly to keep the previous behavior. See Included Extensions.

lexical / @lexical/html / @lexical/selection / @lexical/utils / @lexical/playground — Generalized DOMSlot and DOMRenderExtension override surface (#8519)

DOMSlot is now generalized so child slots can be expressed for arbitrary DOM containers (not just the direct child element), and $createDOM, $updateDOM, $exportDOM, $getDOMSlot etc. all flow through the new override surface. The previously-exported AutocompleteNode in the playground has been replaced by a VisibleLineBreakExtension that demonstrates the new override surface, and markSelection in @lexical/utils has been rewritten on top of the generalized slot API. Custom nodes that override getDOMSlot to return a non-element wrapper now have a real way to express that. See the DOMRenderExtension docs.

New APIs

@lexical/html — DOMImportExtension (experimental) (#8528)

A new DOMImportExtension replaces the legacy node-class importDOM static method with a composable middleware-style API. Rules declare a CSS-selector-style matcher, an optional schema for child handling, and a DOMImportFn that builds Lexical nodes from the matching DOM. Many helpers ship alongside it: $distributeInlineWrapper, ImportSession, DOMPreprocessFn for source-specific cleanup (Word, VS Code, etc.), and full TypeScript types for rules, captures, and contexts. A new dev-examples/dom-import showcase wires it up to a Word/VS Code paste flow. The legacy importDOM continues to work; new code can opt in to DOMImportExtension for richer composition and explicit ordering. The companion ClipboardDOMImportExtension (in @lexical/clipboard) routes text/html pastes through the new pipeline. See the DOMImportExtension guide and the "Migrating from importDOM" section.

@lexical/html / @lexical/playground — Conditional DOM render overrides (#8575)

domOverride now supports disabledForEditor and disabledForSession predicates so render overrides can be conditionally skipped on a per-editor or per-session basis (e.g. only apply during export, or only when a feature flag is on). Useful when one extension provides multiple overrides whose applicability depends on context that isn't known at registration time. See the DOMRenderExtension docs.

lexical — ElementNode import/export of data-lexical-indent (#8536)

ElementNode now uniformly imports and exports the data-lexical-indent attribute so any subclass round-trips its indent level through HTML without having to override exportDOM / importDOM (fixes the long-standing #7729 regression around indented headings/paragraphs).

lexical — Infinite recursion detection in update listeners (#8542)

Update listeners and update transforms that re-trigger themselves are now caught by a runtime guard that throws a descriptive error after a configurable number of nested updates, instead of hanging the tab. See Updates and Listeners.

@lexical/history — canUndo / canRedo ReadonlySignals (#8465)

HistoryExtension's output now exposes canUndo and canRedo as ReadonlySignal<boolean>s so toolbar buttons can subscribe directly without polling historyState or duplicating the priority/state bookkeeping. See Extension Signals.

@lexical/history — maxDepth option (#8537)

HistoryExtension accepts a new maxDepth option that bounds the undo stack. Older entries are evicted FIFO when the depth is exceeded. See the new "Tuning HistoryExtension for memory and long sessions" docs.

@lexical/markdown — $convertSelectionToMarkdownString (#8395)

Counterpart to $convertToMarkdownString that serializes only the current selection (or any caller-provided range) to a markdown string, useful for "copy as markdown" toolbar actions and for selection-scoped AI prompts.

lexical / @lexical/list — $setFormatFromDOM (#8460)

A new $setFormatFromDOM helper extracts text-align / dir from a DOM element and applies it to an ElementNode. ListItemNode.importDOM now uses it to correctly import alignment for list items pasted from external sources.

@lexical/list — ListNode.createListItemNode (#8427)

ListNode exposes a createListItemNode factory that is now used by children-normalization, so ListNode subclasses can supply a custom ListItemNode subclass without monkey-patching the normalizer.

@lexical/rich-text — escapeFormatTriggers on RichTextExtension (#8383)

RichTextExtension now accepts an escapeFormatTriggers config (a ReadonlySignal<EscapeFormatTriggerConfig>) that opts text-format escape behavior in/out per trigger (enter, click, arrow, space, tab). With it enabled, typing at the boundary of a formatted text node "escapes" the active format (bold, italic, code, etc.), matching the typing experience in Google Docs / Notion. Disabled by default. See Included Extensions.

@lexical/extension / @lexical/playground — ClickAfterLastBlockExtension (#8549)

New ClickAfterLastBlockExtension inserts a fresh paragraph when the user clicks below the last block (and places the caret in it), so single-block editors and short documents don't strand the user with no easy way to start a new line.

@lexical/code-prism / @lexical/code-shiki — null Tokenizer.defaultLanguage (#8553)

Tokenizer.defaultLanguage may now be null to indicate that code blocks without an explicit language should round-trip through markdown without injecting a default language tag (e.g. ```javascript). Preserves CommonMark's "no fence info" form on export.

@lexical/code-core / @lexical/code-shiki / @lexical/code-prism — Outdent space-indented code lines (#8445)

Shift+Tab in a code block now correctly outdent...

Read more

v0.44.0

Choose a tag to compare

@etrepum etrepum released this 27 Apr 14:40

v0.44.0 is a monthly release with a few minor breaking changes (deprecations), a notable new experimental DOMRenderExtension API for overriding DOM create/update/export, easier to use priority levels for registering commands, a pagination demonstration and convenient html import/export in the playground, better drag and drop support, automatic inline of CSS for HTML import, consistent CSP-safe style updates, and a wide range of bug fixes and other new features.

Breaking Changes

Deprecation of @lexical/offset OffsetView (#8350)

OffsetView / $createOffsetView never worked correctly, has no documentation, no tests, and no usage in the monorepo. It has been deprecated. The only function still in use, $createChildrenArray, is now exported from lexical itself — import it from lexical rather than @lexical/offset going forward.

Code block escape logic moved to CodeExtension (#8360)

The "press Enter three times to exit a CodeNode" logic has moved from CodeNode.insertNewAfter to a KEY_ENTER_COMMAND listener registered by CodeExtension. This fixes paste-with-blank-lines incorrectly escaping the code block. Using CodeNode without CodeExtension is now deprecated — a backwards-compatible shim with a dev-mode warning is in place, but the node is expected to stop working without CodeExtension in a future version.

New APIs

lexical / @lexical/html — Extensible DOM rendering and export (experimental) (#8353, #8368)

A new DOMRenderExtension (in @lexical/html) and EditorDOMRenderConfig (in lexical) parameterize $createDOM, $updateDOM, $exportDOM, $getDOMSlot, $shouldInclude, and $shouldExclude so that node rendering and export can be overridden via composable middleware. Use domOverride(nodes, config) to target one or more node classes, predicates, or '*' (wildcard), and call $next() from inside an override to delegate to the underlying implementation.

A new $decorateDOM(nextNode, prevNode, dom, editor) hook runs after a node is created/updated and after its children have been reconciled, intended for in-place attribute or DOM tweaks. Override ordering is a well-defined topological sort: wildcards highest, then predicates, then class hierarchy (subclass before superclass), with extension dependency order and registration order as tiebreakers.

Render-context APIs are also exposed: $withRenderContext, $getRenderContextValue, contextValue, contextUpdater, RenderContextRoot, RenderContextExport. New traversal helpers $generateDOMFromNodes and $generateDOMFromRoot populate a container using the configured render config. CreateEditorArgs.dom accepts a Partial<EditorDOMRenderConfig> to override rendering at editor creation. $isLexicalNode is also exported from lexical.

lexical — COMMAND_PRIORITY_BEFORE_* priorities (#8375)

Five new command priorities — COMMAND_PRIORITY_BEFORE_EDITOR, _BEFORE_LOW, _BEFORE_NORMAL, _BEFORE_HIGH, _BEFORE_CRITICAL — register listeners that run most-recently-registered first, in front of the existing first-registered-first listeners at the same priority. Ordering is: highest priority first, then all BEFORE_* listeners (LIFO), then legacy listeners (FIFO). The new priorities are almost always what you want for new code; legacy priorities are unchanged so this is purely additive. See the new "Priorities and ordering" docs.

@lexical/code-prism — CodePrismExtension & @lexical/code-shiki — CodeShikiExtension (#8346)

Both packages now export proper extensions (CodePrismExtension, CodeShikiExtension) with {disabled, tokenizer} signal-based configuration that can be toggled and hot-swapped at runtime. The legacy CodeHighlighterShikiExtension is kept as a deprecated backwards-compatible shim.

@lexical/link / @lexical/react — Custom AutoLink punctuation (#8378)

AutoLinkExtension, registerAutoLink, and LexicalAutoLinkPlugin now accept an optional punctuation setting to override the default boundary characters (e.g. to allow colon-delimited matches without breaking URLs that contain colons).

@lexical/utils — removeEmptyDestination for split-at-caret (#8384)

SplitAtPointCaretNextOptions (used by $insertNodeToNearestRootAtCaret and $splitAtPointCaretNext) gains a removeEmptyDestination option for finer control over whether an empty destination is replaced entirely.

@lexical/clipboard — Shared drop handlers (#8373)

New $handleRichTextDrop and $handlePlainTextDrop helpers implement Lexical-aware text drag-and-drop, including same-block drags, cross-TextNode and cross-block drops, DecoratorNode preservation, and cross-editor drags. registerRichText and registerPlainText now wire these by default.

Stable APIs (no longer @experimental) (#8354)

@lexical/extension (since v0.36.1, Sep 2025) and the NodeState API in lexical (since v0.26.0, Feb 2025) are now formally stable.

Deprecations

  • @lexical/offset OffsetView / $createOffsetView (use $createChildrenArray from lexical) (#8350)
  • @lexical/text $findTextIntersectionFromCharacters — broken, untested, undocumented (#8364)
  • CodeHighlighterShikiExtension / CodeHighlighterShikiConfig (use CodeShikiExtension) (#8346)
  • Using CodeNode without CodeExtension (#8360)

Highlights

Core:

  • ⚠️ #8350 Deprecate OffsetView; export $createChildrenArray from lexical
  • 🆕 #8353 #8368 EditorDOMRenderConfig, DOMRenderExtension, domOverride, $decorateDOM, render context, $generateDOMFromNodes, $generateDOMFromRoot, $isLexicalNode, CreateEditorArgs.dom
  • 🆕 #8375 COMMAND_PRIORITY_BEFORE_* priorities for last-registered-called-first ordering
  • 🧹 #8354 Remove @experimental from Extension and NodeState APIs
  • ✅ #8356 Workaround for synchronous Firefox focus edge case where deferred callbacks were silently discarded
  • ✅ #8349 Resolve Firefox arrow-key navigation skipping over collapsible content
  • ✅ #8361 Fall back to non-shifted key matching for Option+number shortcuts on macOS

Clipboard / Rich Text / Plain Text:

  • ✅ #8373 Drag-and-drop within the same block now correctly moves the dragged text; DecoratorNodes and formatting are preserved across drops; cross-editor drags work even with two Lexical versions on the same page

Code:

  • ⚠️ #8360 Move three-Enter code-block escape logic to a KEY_ENTER_COMMAND listener in CodeExtension; fixes blank-line paste escaping the code block
  • 🆕 #8346 Add CodePrismExtension (@lexical/code-prism) and CodeShikiExtension (@lexical/code-shiki)
  • 🧹 #8330 Upgrade shiki to ^4.0.2

Extension:

  • ✅ #8328 Remove JSX/react type dependency from DecoratorTextNode
  • ✅ #8377 LexicalExtensionEditorComposer no longer disposes the editor on unmount; lifetime is the caller's responsibility (fixes nested/caption editors after remount)

HTML / Table:

  • 🆕 #8326 Inline CSS from <style> tags during HTML import (fixes paste from Excel, Outlook, etc.)
  • ✅ #8313 Merge nested lists into the parent <li> during HTML export, eliminating duplicate ordered-list numbering

Link:

  • 🆕 #8378 Allow custom punctuation for AutoLink boundaries

List:

  • ✅ #8382 Ensure ListItemNode always has a ListNode parent (orphans are merged or floated to root)
  • ✅ #8390 Toggle checklist items on mobile tap (iOS Safari / Android Chrome) via pointerup
  • ✅ #8313 Merge nested lists into parent <li> on HTML export

Markdown:

  • ✅ #8332 Replace negative lookbehind assertions for Safari < 16.4 compatibility
  • ✅ #8365 Add an undo history entry for markdown shortcut transforms so undo no longer also undoes the triggering keystroke
  • ✅ #8311 Escape ordered-list pattern (\d+\. ) inside bullet/check list item exports to fix double-escape on round-trip
  • ✅ #8381 Code spans take precedence over inline formatting in shortcut transforms (CommonMark behavior)

Playground:

  • 🆕 #8322 New PagesExtension with PageNode, PageContentNode, PageSetupNode for paginated documents and print-friendly output
  • 🆕 #8323 Animations for collapsible section expand/collapse and arrow rotation
  • 🆕 #8379 #8386 "Convert to HTML" toolbar button (round-trips through $generateHtmlFromNodes / $generateNodesFromDOM); HTML view formatted with prettier
  • 🆕 #8346 Migrate eight playground plugins to extensions (PageBreak, Twitter, YouTube, Figma, TabFocus, Collapsible, SpecialText, code-highlight) and remove the dead LinkPlugin and StickyPlugin files
  • ✅ #8385 NodeContextMenuPlugin falls back to the root node when the click target has no Lexical node, instead of showing an empty menu
  • ✅ #8388 Use @floating-ui/react for FloatingLinkEditorPlugin positioning; correctly handles all viewport boundaries and Firefox focusNode.parentElement quirks
  • ✅ #8392 Show floating link editor for single-character links (right-bias adjacent-link lookup)

React:

  • ✅ #8329 Prevent error when editorRef is null in LexicalEditorRefPlugin
  • ✅ #8366 Add yjs as an optional peer dependency for Yarn PnP compatibility
  • ✅ #8377 Don't auto-dispose editor from LexicalExtensionEditorComposer
  • ✅ #8385 Root-node fallback in NodeContextMenuPlugin

Text:

  • 🧹 #8364 Add deprecation notice to $findTextIntersectionFromCharacters

Utils:

  • ✅ #8384 Clean up and test $insertNodeToNearestRootAtCaret edge cases; add removeEmptyDestination to SplitAtPointCaretNextOptions

Refactor (CSP-safe styles):

  • 🧹 #8372 Replace runtime cssText / setAttribute('style', …) writes with property-based style updates across lexical, @lexical/code-core, @lexical/list, @lexical/table, @lexical/yjs, and the playground

Website:

  • 🆕 #8316 Add more examples to the demos gallery and statically capture screenshots with Playwrigh...
Read more