mrkeyoor.com_
Wed 23 Sept 00:34 UTC
npmWeb Frontendupdated 22 Sept 2026

lexical review

Lexical 0.49.0 is Meta's state engine for building text editors, not a finished rich-text widget. The core owns immutable editor state, selection, commands, transforms, nodes, and DOM reconciliation. React bindings, history, lists, links, tables, Markdown, HTML, and Yjs support come from separate `@lexical` packages. This release completes the built-in node move to `$config()`, changes command typing, and adds table behavior alongside selection, HTML, Markdown, and compiled-build fixes.

Verdict

Lexical 0.49.0 installed in 2.5 seconds and its core alone measured 58.3 KB gzipped in our sandbox, before the React and feature packages a real editor normally needs. Choose it for a custom editor product with migration capacity, not for a ready-made field or a quiet pre-1.0 API.

We installed it

Lab card: what happened when we installed lexicalScreenshot of lexical documentation
Install✓ · 2.5s2 packages on disk · 4 MB
ImportESM import works · require() works · CommonJS package with exports map
Browser58.3 KBgzipped (182.2 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does lexical install cleanly?

Yes. In a fresh container with an empty cache, npm install lexical finished in 3 seconds, leaving 2 packages and 4 MB on disk. npm audit reported no known vulnerabilities.

How much does lexical add to a browser bundle?

58.3 KB gzipped (182.2 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does lexical work with both ESM and CommonJS?

Yes. Both import 'lexical' and require('lexical') worked in Node 22 in our run. The package is published as CommonJS with an exports map.

Does lexical include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

lexical or @tiptap/core: which should you use?

@tiptap/core: Use it for a ProseMirror-based headless editor with starter kits and a broad extension catalog. Lexical 0.49.0 installed in 2.5 seconds and its core alone measured 58.3 KB gzipped in our sandbox, before the React and feature packages a real editor normally needs.

When should you not use lexical?

You need a drop-in rich-text field. Lexical supplies the engine, while toolbar controls, styling, persistence, validation, and most editing behavior remain application work.

API stability2/5Core concepts such as editor updates, snapshot reads, commands, node transforms, and registered nodes remain recognizable across releases. Version 0.49.0 still contains ordinary-release breaking changes: built-in nodes moved to `$config()`, several static methods disappeared from those classes, and command payload types became invariant. A team must pin the complete package family and budget code and document migrations for monthly upgrades.
Docs5/5Lexical's official site has a conceptual guide, React and vanilla quick starts, API pages for individual packages, node-authoring material, serialization guidance, browser floors, examples, and a working playground. The version 0.49.0 release notes name breaking changes and replacement APIs. Search remains tricky because examples draw from several `@lexical` packages and older posts can show static node methods replaced by `$config()`.
Maintenance5/5Version 0.49.0 shipped on July 30, 2026, and GitHub records a repository push on August 25, 2026. The unarchived project has 23,793 stars and GitHub's combined counter shows 336 open issues and pull requests. The current release includes fixes for compiled node classes, selections, tables, Markdown, HTML, and code highlighting. Activity is high, though that same release pace raises upgrade cost.
Ecosystem5/5The npm endpoint counted 4,978,285 core-package downloads in the latest completed week. Official packages cover React, plain and rich text, history, lists, links, tables, Markdown, HTML, code editing, and Yjs collaboration. The playground and gallery make compositions visible. Integrations are spread across version-coupled packages, so an application's dependency policy must keep the whole Lexical set aligned.

Use it if

  • You are building an editor product whose nodes, selection rules, commands, toolbar, and output format need application-specific behavior.
  • A framework-neutral editing engine is useful now, with official React integration available without making React the core data model.
  • Structured JSON state and custom nodes are a better fit than treating mutable `contenteditable` HTML as the document source.
  • Tables, Markdown, HTML conversion, history, or Yjs collaboration can come from coordinated first-party packages pinned to one version.
Skip it if

Setup reality

We installed Lexical 0.49.0 in 2.5 seconds in a fresh Node 22 container. npm left 2 packages and 4 MB on disk. The core package is 3,476 KB unpacked, has 1 direct dependency and 1 peer dependency, bundles TypeScript declarations, and produced 0 audit findings. Both require() and ESM import worked. Our browser build measured 182.2 KB minified and 58.3 KB gzipped.

A React editor needs more than the core. Install matching 0.49.0 releases of @lexical/react and the feature packages you import, commonly rich text or plain text plus history. The core declares TypeScript >=5.2 as its peer. Every editor needs a namespace, error callback, registered node classes, theme CSS, and a client-side root element. LexicalComposer handles attachment in React; server rendering can cover surrounding markup but cannot attach contenteditable.

State access has strict contexts. Read immutable snapshots through editorState.read(). Create or mutate nodes inside editor.update() or a command listener; dollar-prefixed helpers depend on an active Lexical context. Update listeners can fire for each edit, so debounce remote saves and inspect tags to avoid persisting history playback, imports, or collaboration echoes as new user changes. Keep every cleanup function returned by a registration call because remounts otherwise accumulate commands and transforms.

Release 0.49.0 ports built-in nodes to $config() and removes several static methods from those classes. It also makes LexicalCommand<T> payloads invariant and changes redundant generic call sites. Persist editorState.toJSON() rather than live nodes, register custom classes before parsing saved state, and write migrations before changing their serialized shape. Accessibility claims do not cover the toolbar you build; label controls and test focus, keyboard commands, screen readers, and read-only behavior in your own composition.

Patterns

Mount a framework-neutral editor create-editor

import { createEditor } from 'lexical';

const editor = createEditor({
  namespace: 'CommentEditor',
  nodes: [],
  onError(error) { console.error(error); },
});

const rootElement = document.getElementById('editor');
rootElement.contentEditable = 'true';
editor.setRootElement(rootElement);

The core does not create or style the root. `@lexical/react` supplies composer and content-editable components for the same attachment job.

Insert a paragraph into an update insert-initial-content

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

editor.update(() => {
  const paragraph = $createParagraphNode();
  paragraph.append($createTextNode('Start writing...'));
  $getRoot().append(paragraph);
});

Dollar-prefixed node helpers require a writable Lexical context. Calling them outside `editor.update()` or an equivalent callback throws.

Read plain text from a snapshot read-plain-text

const text = editor.getEditorState().read(() => {
  return $getRoot().getTextContent();
});

console.log(text);

An EditorState is immutable. The read callback sees one consistent version and does not permit node mutation.

Queue persistence after state changes listen-for-updates

const removeListener = editor.registerUpdateListener(({ editorState, tags }) => {
  const text = editorState.read(() => $getRoot().getTextContent());
  queueSave({ json: editorState.toJSON(), text, tags: [...tags] });
});

removeListener();

The callback may run on every edit. Debounce network writes, filter your own update tags, and call the returned cleanup during teardown.

Store Lexical JSON serialize-state

const payload = JSON.stringify(editor.getEditorState().toJSON());
await saveDocument(payload);

The JSON includes node type and version data. Treat it as an editor-owned format and retain migrations for custom node changes.

Parse previously stored state restore-state

const serialized = await loadDocument();
const nextState = editor.parseEditorState(serialized);
editor.setEditorState(nextState);

Every custom node class must be registered before parsing. Missing or incompatible node definitions can prevent faithful document restoration.

Toggle bold for a range selection format-selection

import { $getSelection, $isRangeSelection } from 'lexical';

editor.update(() => {
  const selection = $getSelection();
  if ($isRangeSelection(selection)) selection.formatText('bold');
});

A selection can be null or another selection class. Toolbar state must check for a range instead of assuming selected text exists.

Handle a typed application command register-command

import { COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical';

const INSERT_MENTION_COMMAND = createCommand<string>('INSERT_MENTION_COMMAND');
const unregister = editor.registerCommand(
  INSERT_MENTION_COMMAND,
  (username) => {
    const selection = $getSelection();
    if ($isRangeSelection(selection)) selection.insertText(`@${username}`);
    return true;
  },
  COMMAND_PRIORITY_EDITOR,
);
editor.dispatchCommand(INSERT_MENTION_COMMAND, 'ada');

Returning true prevents lower-priority handlers from receiving the command. Version 0.49.0 makes command payload typing invariant.

Normalize text with a guarded transform transform-text-nodes

import { TextNode } from 'lexical';

const removeTransform = editor.registerNodeTransform(TextNode, (node) => {
  const text = node.getTextContent();
  if (text.includes('  ')) node.setTextContent(text.replaceAll('  ', ' '));
});

Transforms repeat until state settles. Write only when content must change, or the transform can keep scheduling itself.

Toggle editing and toolbar visibility make-read-only

editor.setEditable(false);

const removeEditableListener = editor.registerEditableListener((editable) => {
  toolbar.hidden = !editable;
});

editor.setEditable(true);

Read-only mode changes editor interaction; it does not authorize access. Enforce permissions and validate submitted content on the server.

Mark a programmatic document import tag-an-update

editor.update(
  () => {
    $getRoot().clear();
    $getRoot().append($createParagraphNode().append($createTextNode('Imported')));
  },
  { tag: 'document-import' },
);

Listeners receive update tags. Use an application tag to keep imports or remote sync from being mistaken for a fresh user edit.

Remove registrations and detach the root teardown-editor

removeListener();
unregister();
removeTransform();
removeEditableListener();
editor.setRootElement(null);

Registration methods return cleanup functions. Calling all 4 prevents listeners, commands, and transforms from multiplying after component remounts.

Alternatives

PackageRegistryPick it when
@tiptap/corenpmUse it for a ProseMirror-based headless editor with starter kits and a broad extension catalog.
prosemirror-statenpmUse the lower-level ProseMirror modules when schema constraints and its mature plugin ecosystem are the priority.
slatenpmUse it for a React-centered model when the team already knows Slate elements, leaves, and operations.

More web frontend guides

postcss · react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.