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)