Skip to main content

Theming

Lexical tries to make theming straight-forward, by providing a way of passing a customizable theming object that maps CSS class names to the editor on creation. Here's an example of a plain-text theme:

const exampleTheme = {
paragraph: 'editor-paragraph',
};

In your CSS, you can then add something like:

.editor-placeholder {
color: #999;
overflow: hidden;
position: absolute;
top: 15px;
left: 15px;
user-select: none;
pointer-events: none;
}

.editor-paragraph {
margin: 0 0 15px 0;
position: relative;
}

Put the theme on your root extension. The same theme configuration works with React and vanilla JavaScript:

import {PlainTextExtension} from '@lexical/plain-text';
import {defineExtension} from 'lexical';

export const appExtension = defineExtension({
name: 'MyEditor',
namespace: 'MyEditor',
dependencies: [PlainTextExtension],
theme: exampleTheme,
});

In React, pass that stable extension to LexicalExtensionComposer. This example places its own ContentEditable, so it disables the composer's default one:

import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer';
import {appExtension} from './appExtension';

export default function Editor() {
return (
<LexicalExtensionComposer extension={appExtension} contentEditable={null}>
<div style={{position: 'relative'}}>
<ContentEditable
aria-label="Plain text editor"
aria-placeholder="Enter some text..."
placeholder={<div className="editor-placeholder">Enter some text...</div>}
/>
</div>
</LexicalExtensionComposer>
);
}

In vanilla JavaScript, build the editor from the same extension and attach it to your editable element:

import {buildEditorFromExtensions} from '@lexical/extension';
import {appExtension} from './appExtension';

const editor = buildEditorFromExtensions(appExtension);
editor.setRootElement(document.getElementById('editor'));
// Call editor.dispose() when removing this editor permanently.

A theme supplies class names; it does not install features or include CSS. For example, to use the heading and quote styles below, add RichTextExtension in place of PlainTextExtension. Add the corresponding extensions for lists, links, and code highlighting when you need those features.

Merging extension themes​

Every extension can supply a theme. Lexical recursively merges these themes in dependency order: dependencies contribute first, and extensions that depend on them can override individual entries. Your root extension can therefore customize an extension's styles while keeping the rest of its theme, including nested entries such as text and heading.

const EmphasisExtension = defineExtension({
name: 'Emphasis',
theme: {
text: {
bold: 'emphasis-bold',
italic: 'emphasis-italic',
},
},
});

const appExtension = defineExtension({
name: 'MyEditor',
dependencies: [PlainTextExtension, EmphasisExtension],
theme: {
paragraph: 'editor-paragraph',
text: {bold: 'editor-bold'},
},
});

// The merged theme is:
// {
// paragraph: 'editor-paragraph',
// text: {bold: 'editor-bold', italic: 'emphasis-italic'},
// }

When two themes set the same entry, the later value replaces the earlier one; class-name strings are not concatenated. There is no need to spread a dependency's theme into your own.

Available theme entries​

Many of the Lexical's core nodes also accept theming properties. Here's a more comprehensive theming object:

const exampleTheme = {
paragraph: 'editor-paragraph',
quote: 'editor-quote',
heading: {
h1: 'editor-heading-h1',
h2: 'editor-heading-h2',
h3: 'editor-heading-h3',
h4: 'editor-heading-h4',
h5: 'editor-heading-h5',
h6: 'editor-heading-h6',
},
list: {
nested: {
listitem: 'editor-nested-listitem',
},
ol: 'editor-list-ol',
ul: 'editor-list-ul',
listitem: 'editor-listItem',
listitemChecked: 'editor-listItemChecked',
listitemUnchecked: 'editor-listItemUnchecked',
},
hashtag: 'editor-hashtag',
image: 'editor-image',
link: 'editor-link',
text: {
bold: 'editor-textBold',
code: 'editor-textCode',
italic: 'editor-textItalic',
strikethrough: 'editor-textStrikethrough',
subscript: 'editor-textSubscript',
superscript: 'editor-textSuperscript',
underline: 'editor-textUnderline',
underlineStrikethrough: 'editor-textUnderlineStrikethrough',
},
code: 'editor-code',
codeHighlight: {
atrule: 'editor-tokenAttr',
attr: 'editor-tokenAttr',
boolean: 'editor-tokenProperty',
builtin: 'editor-tokenSelector',
cdata: 'editor-tokenComment',
char: 'editor-tokenSelector',
class: 'editor-tokenFunction',
'class-name': 'editor-tokenFunction',
comment: 'editor-tokenComment',
constant: 'editor-tokenProperty',
deleted: 'editor-tokenProperty',
doctype: 'editor-tokenComment',
entity: 'editor-tokenOperator',
function: 'editor-tokenFunction',
important: 'editor-tokenVariable',
inserted: 'editor-tokenSelector',
keyword: 'editor-tokenAttr',
namespace: 'editor-tokenVariable',
number: 'editor-tokenProperty',
operator: 'editor-tokenOperator',
prolog: 'editor-tokenComment',
property: 'editor-tokenProperty',
punctuation: 'editor-tokenPunctuation',
regex: 'editor-tokenVariable',
selector: 'editor-tokenSelector',
string: 'editor-tokenSelector',
symbol: 'editor-tokenProperty',
tag: 'editor-tokenProperty',
url: 'editor-tokenOperator',
variable: 'editor-tokenVariable',
},
};