A text editor framework that does things differently.
Lexical is a lean text editor framework. It is very lightweight, and exposes a set of modular packages that can be used to add common features like lists, links and tables.
Reliable
Lexical is comprised of editor instances that each attach to a single content editable element. A set of editor states represent the current and pending states of the editor at any given time.
Accessible
Lexical is designed for everyone. It follows best practices established in WCAG and is compatible with screen readers and other assistive technologies.
Fast
Lexical is minimal. It doesn't directly concern itself with UI components, toolbars or rich-text features and markdown. The logic for these features can be included via a plugin interface.
Lexical is framework agnostic
Lexical can be used with Vanilla JavaScript. You don't need libraries or frameworks. Just create an editor, attach it to a DOM element, and start editing.
import {registerPlainText} from '@lexical/plain-text';
import {
$createParagraphNode,
$createTextNode,
$getRoot,
createEditor,
} from 'lexical';
const theme = {
paragraph: 'editor-paragraph',
};
const editor = createEditor({
namespace: 'VanillaEditor',
onError: (error) => console.error(error),
theme,
});
editor.setRootElement(document.getElementById('editor'));
registerPlainText(editor);
editor.update(() => {
const root = $getRoot();
const paragraph = $createParagraphNode();
paragraph.append($createTextNode('This is boilerplate text...'));
root.append(paragraph);
});
Are you a React fan?
Lexical provides React components and Plugins that make it easy to build editors. We are using the RichText and History plugins in this example.
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
import {isMacOs, isMobile, isTablet} from 'react-device-detect';
import './styles.css';
const theme = {
paragraph: 'editor-paragraph',
text: {
bold: 'editor-textBold',
italic: 'editor-textItalic',
underline: 'editor-textUnderline',
},
};
function onError(error) {
console.error(error);
}
export default function ReactEditor() {
const initialConfig = {
namespace: 'PluginsEditor',
nodes: [HeadingNode, QuoteNode],
onError,
theme,
};
const placeholderText =
isMobile || isTablet
? 'Tap to edit'
: isMacOs
? '⌘ + z to undo, ⌘ + shift + z to redo, ⌘ + b for bold, ⌘ + i for italic, ⌘ + u for underline'
: 'Ctrl + z to undo, Ctrl + y to redo, Ctrl + b for bold, Ctrl + i for italic, Ctrl + u for underline';
return (
<LexicalComposer initialConfig={initialConfig}>
<div className="plugins-editor">
<RichTextPlugin
ErrorBoundary={LexicalErrorBoundary}
contentEditable={<ContentEditable className="editor-input" />}
placeholder={
<div className="editor-placeholder">
{placeholderText}
</div>
}
/>
<HistoryPlugin />
</div>
</LexicalComposer>
);
}
Build your own plugin
Create custom functionality by listening to and dispatching commands. Here's a toolbar plugin that adds formatting controls.
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {
$createHeadingNode,
$createQuoteNode,
$isHeadingNode,
} from '@lexical/rich-text';
import {$setBlocksType} from '@lexical/selection';
import {$findMatchingParent, mergeRegister} from '@lexical/utils';
import {
$createParagraphNode,
$getSelection,
$isRangeSelection,
$isRootOrShadowRoot,
CAN_REDO_COMMAND,
CAN_UNDO_COMMAND,
COMMAND_PRIORITY_LOW,
FORMAT_TEXT_COMMAND,
REDO_COMMAND,
SELECTION_CHANGE_COMMAND,
UNDO_COMMAND,
} from 'lexical';
import {useCallback, useEffect, useState} from 'react';
function applyBlockType(editor, type) {
const factories = {
h1: () => $createHeadingNode('h1'),
h2: () => $createHeadingNode('h2'),
h3: () => $createHeadingNode('h3'),
paragraph: () => $createParagraphNode(),
quote: () => $createQuoteNode(),
};
editor.update(() => {
$setBlocksType($getSelection(), factories[type]);
});
}
function ToolbarPlugin() {
const [editor] = useLexicalComposerContext();
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const [isUnderline, setIsUnderline] = useState(false);
const [blockType, setBlockType] = useState('paragraph');
const $updateToolbar = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const anchor = selection.anchor.getNode();
const element =
$findMatchingParent(anchor, (e) => {
const parent = e.getParent();
return parent !== null && $isRootOrShadowRoot(parent);
}) || anchor.getTopLevelElementOrThrow();
setBlockType(
$isHeadingNode(element) ? element.getTag() : element.getType(),
);
setIsBold(selection.hasFormat('bold'));
setIsItalic(selection.hasFormat('italic'));
setIsUnderline(selection.hasFormat('underline'));
}
}, []);
useEffect(() => {
return mergeRegister(
editor.registerUpdateListener(({editorState}) => {
editorState.read(() => $updateToolbar(), {editor});
}),
editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
$updateToolbar();
return false;
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
CAN_UNDO_COMMAND,
(payload) => {
setCanUndo(payload);
return false;
},
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
CAN_REDO_COMMAND,
(payload) => {
setCanRedo(payload);
return false;
},
COMMAND_PRIORITY_LOW,
),
);
}, [editor, $updateToolbar]);
return (
<div className="toolbar">
<select
value={blockType}
onChange={(e) => applyBlockType(editor, e.target.value)}>
<option value="paragraph">Normal</option>
<option value="h1">Heading 1</option>
<option value="h2">Heading 2</option>
<option value="h3">Heading 3</option>
<option value="quote">Quote</option>
</select>
<div className="divider" />
<button
disabled={!canUndo}
onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
title="Undo">
↩
</button>
<button
disabled={!canRedo}
onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
title="Redo">
↪
</button>
<div className="divider" />
<button
className={isBold ? 'active' : ''}
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')}
title="Bold">
B
</button>
<button
className={isItalic ? 'active' : ''}
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')}
title="Italic"
style={{fontStyle: 'italic'}}>
I
</button>
<button
className={isUnderline ? 'active' : ''}
onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')}
title="Underline"
style={{textDecoration: 'underline'}}>
U
</button>
</div>
);
}
What's next?