src/skills/web-editor-lexical/SKILL.md
Extensible text editor framework by Meta
npx skillsauth add agents-inc/skills web-editor-lexicalInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Lexical is a lightweight (22kb min+gzip) extensible text editor framework. Use
LexicalComposerfor React setup with plugins as child components. Extend via the node system (ElementNode, TextNode, DecoratorNode), command system (createCommand + priorities), and transforms. EditorState is immutable -- all mutations happen insideeditor.update(). Use$-prefixed functions only inside update/read closures. Current: v0.42.x (pre-1.0)
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST call $-prefixed functions ($getRoot, $getSelection, $createTextNode) ONLY inside editor.update() or editor.read() closures -- calling them outside throws runtime errors)
(You MUST register custom nodes in the nodes array of initialConfig -- unregistered nodes cause silent failures or runtime errors)
(You MUST return a cleanup function from useEffect when registering commands, transforms, or listeners -- Lexical register methods return unsubscribe functions)
(You MUST include preconditions in transforms to prevent infinite loops -- a transform that unconditionally modifies its target node re-triggers itself)
</critical_requirements>
Auto-detection: Lexical, lexical, @lexical/react, @lexical/rich-text, @lexical/list, @lexical/code, @lexical/link, @lexical/html, @lexical/headless, LexicalComposer, EditorState, LexicalNode, ElementNode, TextNode, DecoratorNode, createCommand, dispatchCommand, registerCommand, COMMAND_PRIORITY, $getRoot, $getSelection, $createParagraphNode, $createTextNode, RichTextPlugin, OnChangePlugin, HistoryPlugin, useLexicalComposerContext, editor.update, editor.read, registerNodeTransform, exportJSON, importJSON, exportDOM, importDOM, NodeState, createState
When to use:
When NOT to use:
<textarea>)Key patterns covered:
Detailed Resources:
Lexical is an editor framework, not a batteries-included editor. The core is intentionally minimal -- it provides the node tree, selection, reconciler, command system, and update lifecycle. Everything else (toolbars, formatting, lists, links, embeds) is a plugin.
Key architectural principles:
editor.update() closures that clone the state, apply changes, then reconcile to DOM.$-function convention: Functions prefixed with $ (like $getRoot(), $getSelection()) must run inside editor.update() or editor.read() closures -- similar to React hooks requiring a component context.<LexicalComposer>. It accesses the editor via useLexicalComposerContext() and registers commands/transforms/listeners in useEffect.When to use Lexical:
When NOT to use Lexical:
<textarea> is simpler and lighter)The minimal setup uses LexicalComposer wrapping plugin components. Each plugin is a React component that accesses the editor via context.
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
const EDITOR_NAMESPACE = "MyEditor";
const theme = {
paragraph: "editor-paragraph",
text: {
bold: "editor-text-bold",
italic: "editor-text-italic",
},
};
function onError(error: Error) {
console.error(error);
}
const initialConfig = {
namespace: EDITOR_NAMESPACE,
theme,
onError,
nodes: [], // Register custom nodes here
};
export function Editor() {
return (
<LexicalComposer initialConfig={initialConfig}>
<RichTextPlugin
contentEditable={<ContentEditable className="editor-input" />}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
</LexicalComposer>
);
}
Why good: Plugins compose as children, initialConfig centralizes node registration and theming, error boundary catches update errors gracefully
See examples/core.md for the full setup with OnChangePlugin, AutoFocusPlugin, and custom plugins.
$-Function Convention and Update LifecycleAll state reads and mutations use $-prefixed functions inside editor.update() (mutable) or editor.read() (read-only) closures. This ensures state consistency and prevents stale reads.
import { $getRoot, $createParagraphNode, $createTextNode } from "lexical";
// Writing: editor.update() clones state, applies changes, reconciles DOM
editor.update(() => {
const root = $getRoot();
const paragraph = $createParagraphNode();
const text = $createTextNode("Hello world");
paragraph.append(text);
root.append(paragraph);
});
// Reading: editor.read() provides safe read-only access
editor.read(() => {
const root = $getRoot();
const textContent = root.getTextContent();
});
Why good: Immutable state model prevents race conditions, $ prefix signals context requirement (like React hooks), update batching minimizes DOM reconciliation
Gotcha: $-functions called outside update/read closures throw runtime errors. There is no compile-time check.
Commands are the communication bus between plugins, toolbars, and the editor core. Create typed commands, dispatch them from UI, and register listeners with priorities.
import {
createCommand,
COMMAND_PRIORITY_EDITOR,
COMMAND_PRIORITY_LOW,
type LexicalCommand,
} from "lexical";
// Create a typed command
export const INSERT_IMAGE_COMMAND: LexicalCommand<{
src: string;
alt: string;
}> = createCommand("INSERT_IMAGE_COMMAND");
// Dispatch from toolbar or UI
editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
src: "/image.png",
alt: "Photo",
});
Priority levels (higher number = runs first, can intercept):
| Priority | Value | Use case |
| --------------------------- | ----- | ---------------------------------- |
| COMMAND_PRIORITY_CRITICAL | 4 | Emergency overrides |
| COMMAND_PRIORITY_HIGH | 3 | Table navigation, critical plugins |
| COMMAND_PRIORITY_NORMAL | 2 | Standard plugin behavior |
| COMMAND_PRIORITY_LOW | 1 | Default for most plugins |
| COMMAND_PRIORITY_EDITOR | 0 | Base editor behavior |
Return true from a listener to stop propagation to lower-priority listeners.
See examples/core.md for the full command registration pattern with cleanup.
A plugin is a React component that registers commands, transforms, or listeners via useLexicalComposerContext. Always return cleanup functions from useEffect.
import { useEffect } from "react";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import {
COMMAND_PRIORITY_LOW,
FORMAT_TEXT_COMMAND,
type TextFormatType,
} from "lexical";
export function ToolbarPlugin() {
const [editor] = useLexicalComposerContext();
const handleFormat = (format: TextFormatType) => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
};
return (
<div className="toolbar">
<button onClick={() => handleFormat("bold")} type="button">
Bold
</button>
<button onClick={() => handleFormat("italic")} type="button">
Italic
</button>
</div>
);
}
Why good: Plugin accesses editor through context hook, dispatches built-in FORMAT_TEXT_COMMAND, renders null or UI as needed
See examples/core.md for plugins that register commands with useEffect cleanup.
Transforms automatically mutate nodes when conditions are met. They run before DOM reconciliation, making them the most efficient way to react to content changes.
import { TextNode } from "lexical";
// Transform: auto-capitalize first letter of paragraphs
editor.registerNodeTransform(TextNode, (textNode) => {
const text = textNode.getTextContent();
// CRITICAL: Precondition prevents infinite loop
if (text.length > 0 && text[0] !== text[0].toUpperCase()) {
textNode.setTextContent(text[0].toUpperCase() + text.slice(1));
}
});
Why preconditions matter: Without the check, setTextContent marks the node dirty, re-triggering the transform infinitely.
Transform execution order: Leaf nodes first, then element nodes, then RootNode. Multiple transforms produce a single DOM reconciliation.
See examples/core.md for transform registration with cleanup and use cases.
Lexical provides three extendable base nodes for custom content types:
| Base Node | Purpose | Key method |
| ------------------ | --------------------------------------------- | -------------------------------- |
| ElementNode | Container nodes (blockquote, callout) | createDOM(), updateDOM() |
| TextNode | Styled text variants (colored text, mentions) | createDOM(), updateDOM() |
| DecoratorNode<T> | Embedded components (images, videos, polls) | decorate() returns a component |
Every custom node requires:
static getType() -- unique string identifierstatic clone(node) -- create copy for state snapshotscreateDOM() -- return the HTMLElement representationupdateDOM() -- return false if existing DOM can be reusedexportJSON() / static importJSON() -- serializationinitialConfig.nodesimport { DecoratorNode } from "lexical";
import type { LexicalNode, NodeKey, EditorConfig } from "lexical";
export class ImageNode extends DecoratorNode<JSX.Element> {
__src: string;
__alt: string;
static getType(): string {
return "image";
}
static clone(node: ImageNode): ImageNode {
return new ImageNode(node.__src, node.__alt, node.__key);
}
constructor(src: string, alt: string, key?: NodeKey) {
super(key);
this.__src = src;
this.__alt = alt;
}
createDOM(_config: EditorConfig): HTMLElement {
return document.createElement("div");
}
updateDOM(): boolean {
return false;
}
decorate(): JSX.Element {
return <img src={this.__src} alt={this.__alt} />;
}
}
Property convention: Prefix private properties with __ (double underscore) to prevent minification issues. All properties must be JSON-serializable.
See examples/custom-nodes.md for complete ElementNode, TextNode, DecoratorNode examples with serialization and the NodeState API.
Lexical supports JSON (preferred for persistence) and HTML (for display or interop).
import { $generateHtmlFromNodes } from "@lexical/html";
// JSON: lossless round-trip
const json = editor.getEditorState().toJSON();
const jsonString = JSON.stringify(json);
// Restore from JSON
const editorState = editor.parseEditorState(jsonString);
editor.setEditorState(editorState);
// HTML: for rendering or export
editor.read(() => {
const html = $generateHtmlFromNodes(editor, null);
});
JSON vs HTML: JSON preserves the full node tree and is the recommended format for persistence. HTML is lossy (loses custom node properties) but useful for display or email content.
See examples/serialization.md for complete import/export patterns, HTML-to-Lexical conversion, and headless editor usage.
</patterns><decision_framework>
Does your content contain child nodes?
├─ YES → ElementNode (paragraphs, blockquotes, callouts)
└─ NO → Is it text with special formatting or behavior?
├─ YES → TextNode (colored text, mentions)
└─ NO → Is it an embedded component (image, video, widget)?
├─ YES → DecoratorNode (renders arbitrary UI)
└─ NO → Re-evaluate: most content fits one of the above
Need to react to content changes?
├─ YES → Does the reaction modify nodes?
│ ├─ YES → Transform (most efficient, runs before DOM reconciliation)
│ └─ NO → Update listener (read-only, runs after reconciliation)
└─ NO → Need to handle user actions or toolbar clicks?
├─ YES → Command (typed, priority-based, interceptable)
└─ NO → Listener (registerUpdateListener for state observation)
Is this the base editor behavior?
├─ YES → COMMAND_PRIORITY_EDITOR (0)
└─ NO → Is this a standard plugin?
├─ YES → COMMAND_PRIORITY_LOW (1) or COMMAND_PRIORITY_NORMAL (2)
└─ NO → Must it override other plugins (e.g., table navigation)?
├─ YES → COMMAND_PRIORITY_HIGH (3)
└─ NO → Emergency override only?
└─ YES → COMMAND_PRIORITY_CRITICAL (4)
</decision_framework>
<red_flags>
High Priority Issues:
$-prefixed functions outside editor.update() or editor.read() -- causes runtime errors with no compile-time warninginitialConfig.nodes -- custom nodes silently fail or throw when the editor encounters themeditor.update() inside an update listener to modify state -- breaks undo/redo history and causes extra renders; use transforms insteaduseEffect cleanup for register calls -- leaks listeners, causes stale references after component unmountMedium Priority Issues:
editor.setEditorState() without cloning -- can cause unexpected focus changes; use editorState.clone(null) to prevent auto-focusgetType() with a non-unique string -- collides with other nodes, causes deserialization failuresnew MyNode() directly instead of $createMyNode() factory -- bypasses the node replacement system ($applyNodeReplacement)_value instead of __value) -- may be mangled by minifiers, breaking node accessconsole.log in onError callback with no rethrow -- silently swallows editor errors; rethrow or send to error trackingGotchas & Edge Cases:
editor.update() batches synchronously but reconciles asynchronously -- use { discrete: true } option when you need synchronous DOM commit (e.g., before reading DOM measurements)__ prefix convention -- single underscore properties may be mangled by minifiersDecoratorNode.decorate() returns a component that Lexical renders outside the normal React tree -- state management in decorator components needs careonError callback in initialConfig receives errors from update closures -- if you don't rethrow, Lexical tries to recover gracefullyTextNode modes: "token" makes text immutable (like a chip), "segmented" deletes word-by-wordtransition does not work for animations based on node removal -- Lexical reconciles by removing DOM nodes, not hiding them</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST call $-prefixed functions ($getRoot, $getSelection, $createTextNode) ONLY inside editor.update() or editor.read() closures -- calling them outside throws runtime errors)
(You MUST register custom nodes in the nodes array of initialConfig -- unregistered nodes cause silent failures or runtime errors)
(You MUST return a cleanup function from useEffect when registering commands, transforms, or listeners -- Lexical register methods return unsubscribe functions)
(You MUST include preconditions in transforms to prevent infinite loops -- a transform that unconditionally modifies its target node re-triggers itself)
Failure to follow these rules will cause runtime errors, memory leaks, frozen editors, and broken undo/redo history.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events