The term WYSIWYG is very commonly used in software to describe either a rich text editor or a system that gives you the ability to edit text in a rich format. With a WYSIWYG text editor, you essentially get to see what the end result will look like as you’re working on your document. This approach is beneficial, providing immediate feedback, unlike systems where you have to write some markup language like markdown.
WYSIWYG editors are very important features that you can find in many types of software, including content management systems, web builders, complex forms, note-taking tools, kanban boards, and more. As a matter of fact, this article was written in Dropbox Paper, which is a good example of a WYSIWYG editor.
In this article, we’ll use Lexical and React to build a simplified version of the Dropbox Paper editor. You can check out the source code for this article at this repo. Let’s get started!
What is Lexical?
Lexical is a dependency-free, extensible text editor framework that is being actively developed by Facebook at the time of writing. Lexical provides low-level APIs for developers to build their own editors with varying levels of complexity. It’s important to note that at the time of writing this article, Lexical is still in early development. However, in my opinion, the existing plugins are sufficient to build all kinds of great tools.
If you’re familiar with tools like CKEditor, Quill, and ProseMirror, then you’re aware that rich text editors have some common functionalities that are frequently required by developers. Lexical provides these features as a set of individual, modular packages that you can easily pull into your project based on your requirements, eliminating the need rewrite certain functionalities over and over again.
Lexical is not built for any specific platform. Rather, it is designed to be completely cross-platform and framework agnostic, implying that the underlying API can easily be ported to mobile or native desktop while still maintaining the same compatibility with the web version. It can also be seamlessly plugged into different frontend frameworks.
Features of Lexical
Lexical is a fast, reliable, lightweight, and accessible editor engine that aims to provide a great developer experience. Lexical is not opinionated about the appearance or styling of your editor’s UI. You can think of it as a headless, rich text editor that gives you the primitives to build anything you want. Its headless approach makes it highly extensible, allowing you to build new features or refine existing features to best suit your needs.
Some APIs that Lexical exposes include:
- Plain text
- Rich text
- Selection
- History
- Clipboard
- List
- Table
- Code
- Link
Getting started with Lexical
Let’s explore some of the features of Lexical by building a simple clone of the Dropbox Paper editor. The editor consists of a floating toolbar at the bottom, and the whole document is the editor viewport.
We’ll use the React binding for Lexical in this example. To make it easily accessible, we’ll host it on Stackblitz with a link to the complete demo at the end of this article.
To get started, we’ll create a React project and install the necessary dependencies. You can choose whatever method you prefer to start a React project including Create React App, Stackblitz, or CodeSandbox:
$ npm install -S lexical @lexical/react @lexical/utils @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/react-fontawesome clsx
Next, we’ll import the necessary components to create an instance of the editor:
Editor.jsx import React from 'react'; import clsx from 'clsx'; import { $getRoot, $getSelection, $isRangeSelection, FORMAT_TEXT_COMMAND, } from 'lexical'; import { LexicalComposer } from '@lexical/react/LexicalComposer'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { ContentEditable } from '@lexical/react/LexicalContentEditable'; import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'; import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'; import { mergeRegister } from '@lexical/utils'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; function onChange(state) { state.read(() => { const root = $getRoot(); const selection = $getSelection(); console.log(selection); }); } export const Editor = () => { return ( <div className='bg-white relative rounded-sm'> <LexicalComposer initialConfig={{ theme: { paragraph: 'mb-1', // tailwind classes work! }, onError(error) { throw error; }, }} > <RichTextPlugin contentEditable={ <ContentEditable className="h-[450px] outline-none py-[15px] px-2.5 resize-none overflow-hidden text-ellipsis" /> } placeholder={ <div className="absolute top-[15px] left-[10px] pointer-events-none select-none"> Now write something brilliant... </div> } /> <OnChangePlugin onChange={onChange} /> <HistoryPlugin /> </LexicalComposer> </div> ) }
We’ve imported the following components:
LexicalComposer
: The editor root component that is also a context provider for the editor instanceLexicalRichTextPlugin
: Exposes a set of common functionalities that enables rich text editing, including bold, italic, underline, strike-through, alignment text formatting, and copy and pasteLexicalOnChangePlugin
: Executes a callback function whenever the editor state changes, letting you perform actions based on state changeHistoryPlugin
: Provides editor history functionality, which exposes undo and redo commands
Styling the components
In the code block above, we created a simple instance of a Lexical editor and defined the initial config
on LexicalComposer
. In the config
, we can customize certain behaviors of the editor. For example, we can customize the appearance of nodes by defining a theme
object, which maps CSS class names to the editor. If you define a class name on the paragraph
node, the styles that match that class name in your stylesheet will be applied to all corresponding nodes.
A much easier approach is to directly define inline styles using Tailwind CSS classes in our theme
object, as we’ve done in the above code in Editor.jsx
. This approach will also work on any component that you import; just add a className
prop.
Building the Toolbar
component
At this point, we have a simple working editor that we can type text into, but we have no way of controlling what formatting is applied. To address this, let’s build a floating toolbar similar to that in Dropbox Paper:
// Editor.jsx //... const Toolbar = () => { const [editor] = useLexicalComposerContext(); const [isBold, setIsBold] = React.useState(false); const updateToolbar = React.useCallback(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { setIsBold(selection.hasFormat('bold')); } }, [editor]); React.useEffect(() => { return mergeRegister( editor.registerUpdateListener(({ editorState }) => { editorState.read(() => { updateToolbar(); }); }) ); }, [updateToolbar, editor]); return ( <div className="absolute z-20 bottom-0 left-1/2 transform -translate-x-1/2 min-w-52 h-10 px-2 py-2 bg-[#1b2733] mb-4 space-x-2 flex items-center"> <button className={clsx( 'px-1 hover:bg-gray-700 transition-colors duration-100 ease-in', isBold ? 'bg-gray-700' : 'bg-transparent' )} onClick={() => { editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold'); }} > <FontAwesomeIcon icon="fa-solid fa-bold" className="text-white w-3.5 h-3.5" /> </button> {/* ... */} </div> ); };
Let’s break down this code. We created a Toolbar
component that returns a div
element, that is positioned absolute at the bottom of the editor. We define a button
element that represents a toolbar action, i.e., bold
. We’ll also define other action buttons here.
The onClick
event triggers a call to editor.dispatchCommand
, which reacts to certain events like FORMAT_TEXT_COMMAND
and updates the editor state accordingly. The RichTextPlugin
already handles the FORMAT_TEXT_COMMAND
internally, however, we can write our own custom commands using editor.registerCommand
to explicitly handle more complex nodes.
The updateToolbar
function runs every time the editor state changes. With this function, we can control the current state of each text format and show an indicator in the toolbar, either active or inactive.
In the useEffect
callback, we actually register the listeners via registerUpdateListener
. We just need to render the Toolbar
component in the editor, as shown below:
//... export const Editor = () => { return ( <div className='bg-white relative rounded-sm'> <LexicalComposer initialConfig={{ theme: { paragraph: 'mb-1', // tailwind classes work! }, onError(error) { throw error; }, }} > <Toolbar/> <RichTextPlugin contentEditable={ <ContentEditable className="h-[450px] outline-none py-[15px] px-2.5 resize-none overflow-hidden text-ellipsis" /> } placeholder={ <div className="absolute top-[15px] left-[10px] pointer-events-none select-none"> Now write something brilliant... </div> } /> <OnChangePlugin onChange={onChange} /> <HistoryPlugin /> </LexicalComposer> </div> ) }
Now, we can follow the same pattern to add more action buttons to our toolbar. We just need to duplicate the button
element, then change the icon
and onClick
handler to dispatch the correct event. Let’s add an action button for italics text formatting:
// Editor.jsx //... <button className={clsx( 'px-1 hover:bg-gray-700 transition-colors duration-100 ease-in', isItalic ? 'bg-gray-700' : 'bg-transparent' )} onClick={() => { editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic'); }} > <FontAwesomeIcon icon="fa-solid fa-italic" className="text-white w-3.5 h-3.5" /> </button> //...
With that, we’re done! If you’ve followed along, you should have a working prototype of a simple WYSIWYG editor with minimal features that you can build on top of. The Lexical framework ships with a wide variety of prebuilt plugins that you can use to add different functionalities to your editor.
You can check out the complete code for the example in this article on Stackblitz. It includes more formatting features like underline
, strikethrough
, left/right/center/justify alignment
, and more.
Alternatives to Lexical
There are several alternative tools to Lexical that can be used to build text editors and provide similar features.
One example is Draft.js which was also developed at Facebook. Unlike Lexical, the Draft.js framework is built exclusively for React. This implies that its underlying architecture is tightly coupled with the React framework.
While Draft.js is extensible and customizable, it does not offer cross-platform and framework-agnostic benefits like Lexical. Nevertheless, it still comes with a lot of the features needed to build any type of rich text editor. If cross-platform support is not a requirement for your project, I encourage you to explore Draft.js as an alternative to Lexical. It’s worth noting that Draft.js is now in maintenance mode and that new features will not be added.
Another good alternative to Lexical is Editor.js. This open source text editor framework produces JSON output as opposed to raw HTML, making it seamless to render the data on platforms different from the web.
The Editor.js core API provides a lot of the basic tools required to create a powerful rich text editor; however, it’s also extendable and pluggable by design, meaning you can easily import plugins for any feature you need in your editor. Here’s a quick start guide for creating your first text editor with Editor.js.
Conclusion
In this article, we’ve learned how to build a simple, rich text editor using the Lexical framework with React.
Keep in mind that because Lexical is still in early development at the time of writing, the APIs are bound to change over time as it’s being refined with each new version. However, there are a lot of interesting things you can build today with the current provided features and plugins. Thanks for reading, and be sure to leave a comment if you have any questions.
LogRocket: Full visibility into your production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?
Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.
No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
Such a nice post on Rich text editor JavaScript.
Where do you define or import LexicalRichTextPlugin ?