In this article, I’ll share my step-by-step approach to building a full-featured Rich Text Editor in React.
The reason I chose Lexical (over Tiptap or others) is simple: speed ⚡.
But… Lexical by itself can be a bit painful to work with.
It’s low-level, verbose, and you often end up handling too much boilerplate.
That’s why I built LexKit — an open-source layer on top of Lexical.
It’s DX-friendly, type-safe, and ships with ready-to-use templates (even for shadcn).
👉 It gives us the full power of Lexical, but with a lot more out-of-the-box features.
👉 Plug-and-play extensions.
👉 Strong type safety.
📚 Great docs are here → lexkit.dev/docs
🛠️ Installing LexKit
First, install LexKit + Lexical in your project:
npm install @lexkit/editor
And install the required Lexical packages:
npm install lexical @lexical/react @lexical/html @lexical/markdown @lexical/list @lexical/rich-text @lexical/selection @lexical/utils
✨ Creating the Basic Editor
Let’s create a basic editor together:
import { createEditorSystem, boldExtension, italicExtension, historyExtension, listExtension, linkExtension, RichText, } from "@lexkit/editor"; import "./basic-editor.css"; // 1. Define your extensions (as const for type safety) const extensions = [ boldExtension, italicExtension, listExtension, linkExtension, historyExtension, ] as const; // 2. Create typed editor system const { Provider, useEditor } = createEditorSystem<typeof extensions>();
The nice thing?
useEditor
is fully type-safe — it knows exactly what commands you have.
🛠️ Toolbar Example
Here’s a simple toolbar with some text formatting actions:
// Toolbar Component - Shows basic text formatting buttons function Toolbar() { const { commands, activeStates } = useEditor(); return ( <div className="basic-toolbar"> <button onClick={() => commands.toggleBold()} className={activeStates.bold ? "active" : ""} title="Bold (Ctrl+B)" > Bold </button> <button onClick={() => commands.toggleItalic()} className={activeStates.italic ? "active" : ""} title="Italic (Ctrl+I)" > Italic </button> <button onClick={() => commands.toggleUnorderedList()} className={activeStates.unorderedList ? "active" : ""} title="Bullet List" > • List </button> <button onClick={() => commands.toggleOrderedList()} className={activeStates.orderedList ? "active" : ""} title="Numbered List" > 1. List </button> <button onClick={() => commands.undo()} disabled={!activeStates.canUndo} className={!activeStates.canUndo ? "disabled" : ""} title="Undo (Ctrl+Z)" > ↶ Undo </button> <button onClick={() => commands.redo()} disabled={!activeStates.canRedo} className={!activeStates.canRedo ? "disabled" : ""} title="Redo (Ctrl+Y)" > ↷ Redo </button> </div> ); }
📝 Putting It All Together
Now, let’s use our Toolbar
inside the editor provider:
// Main Component export function BasicEditorExample() { return ( <Provider extensions={extensions}> <div className="basic-editor"> <Toolbar /> <RichText classNames={{ container: "basic-editor-container", contentEditable: "basic-content", placeholder: "basic-placeholder", }} placeholder="Start writing your content here..." /> </div> </Provider> ); }
To style it, just copy this CSS into your project:
👉 basic-editor.css
🎉 Boom — you’ve got your first editor running!
Try it out live in the Playground.
📦 Templates
The same way we created the basic editor, you can import more extensions or use prebuilt templates:
Default Template (React + CSS):
👉 Lexkit Default TemplateShadcn-Ready Template:
👉 Lexical + Shadcn Template with LexKit
📚 Docs & Demos
Everything is documented here:
💡 With LexKit, you can build Notion-like editors, blog editors, CMS inputs, or any custom RTE — all while staying type-safe and React-friendly.
If you’re into React + TypeScript, I think you’ll love it ❤️
I'd love to hear your thoughts. Feel free to drop a comment <3
Top comments (0)