A modern, extensible rich text editor built on Meta's Lexical framework with dual-architecture design, featuring both a powerful kernel and React integration. Optimized for AI applications and chat interfaces.
Table of contents
- 🎯 Dual Architecture - Both kernel-based API and React components for maximum flexibility
- ⚛️ React-First - Built for React 19+ with modern hooks and patterns
- 🔌 Rich Plugin Ecosystem - 10+ built-in plugins for comprehensive content editing
- 💬 Chat Interface Ready - Pre-built chat input components with mention support
- ⌨️ Slash Commands - Intuitive
/and@triggered menus for quick content insertion - 📝 Multiple Export Formats - JSON, Markdown, and plain text export capabilities
- 🎨 Customizable UI - Antd-styled components with flexible theming
- 🔗 File & Media Support - Native support for images, files, tables, and more
- 🎯 TypeScript Native - Built with TypeScript for excellent developer experience
- 📱 Modern Build System - Optimized with Vite, lobedocs, and comprehensive testing
To install @lobehub/editor, run the following command:
$ bun add @lobehub/editor$ pnpm add @lobehub/editor[![]back-to-top]
The simplest way to get started with a fully-featured editor:
import {
INSERT_HEADING_COMMAND,
ReactCodeblockPlugin,
ReactImagePlugin,
ReactLinkPlugin,
ReactListPlugin,
} from '@lobehub/editor';
import { Editor, useEditor } from '@lobehub/editor/react';
export default function MyEditor() {
const editor = useEditor();
return (
<Editor
placeholder="Start typing..."
editor={editor}
plugins={[ReactListPlugin, ReactLinkPlugin, ReactImagePlugin, ReactCodeblockPlugin]}
slashOption={{
items: [
{
key: 'h1',
label: 'Heading 1',
onSelect: (editor) => {
editor.dispatchCommand(INSERT_HEADING_COMMAND, { tag: 'h1' });
},
},
// More slash commands...
],
}}
onChange={(editor) => {
// Handle content changes
const markdown = editor.getDocument('markdown');
const json = editor.getDocument('json');
}}
/>
);
}Add more functionality with built-in plugins:
import {
INSERT_FILE_COMMAND,
INSERT_MENTION_COMMAND,
INSERT_TABLE_COMMAND,
ReactFilePlugin,
ReactHRPlugin,
ReactTablePlugin,
} from '@lobehub/editor';
import { Editor, useEditor } from '@lobehub/editor/react';
export default function AdvancedEditor() {
const editor = useEditor();
return (
<Editor
editor={editor}
plugins={[
ReactTablePlugin,
ReactHRPlugin,
Editor.withProps(ReactFilePlugin, {
handleUpload: async (file) => {
// Handle file upload
return { url: await uploadFile(file) };
},
}),
]}
mentionOption={{
items: async (search) => [
{
key: 'user1',
label: 'John Doe',
onSelect: (editor) => {
editor.dispatchCommand(INSERT_MENTION_COMMAND, {
label: 'John Doe',
extra: { userId: 1 },
});
},
},
],
}}
/>
);
}Pre-built component optimized for chat interfaces:
import { ChatInput } from '@lobehub/editor/react';
export default function ChatApp() {
return (
<ChatInput
placeholder="Type a message..."
onSend={(content) => {
// Handle message send
console.log('Message:', content);
}}
enabledFeatures={['mention', 'upload', 'codeblock']}
/>
);
}For advanced use cases, access the underlying kernel directly:
import { IEditor, createEditor } from '@lobehub/editor';
// Create editor instance
const editor: IEditor = createEditor();
// Register plugins
editor.registerPlugin(SomePlugin, { config: 'value' });
// Interact with content
editor.setDocument('text', 'Hello world');
const content = editor.getDocument('json');
// Listen to events
editor.on('content-changed', (newContent) => {
console.log('Content updated:', newContent);
});
// Execute commands
editor.dispatchCommand(INSERT_HEADING_COMMAND, { tag: 'h2' });[![]back-to-top]
| Plugin | Description | Features |
|---|---|---|
| CommonPlugin | Foundation editor components | ReactEditor, ReactEditorContent, ReactPlainText, base utilities |
| MarkdownPlugin | Markdown processing engine | Shortcuts, transformers, serialization, custom writers |
| UploadPlugin | File upload management system | Priority handlers, drag-drop, multi-source uploads |
| Plugin | Description | Features |
|---|---|---|
| ReactSlashPlugin | Slash command menu system | / and @ triggered menus, customizable items, async search |
| ReactMentionPlugin | User mention support | @username mentions, custom markdown output, async user search |
| ReactImagePlugin | Image handling | Upload, display, drag & drop, captions, resizing |
| ReactCodeblockPlugin | Code syntax highlighting | Shiki-powered, 100+ languages, custom themes, color schemes |
| ReactListPlugin | List management | Ordered/unordered lists, nested lists, keyboard shortcuts |
| ReactLinkPlugin | Link management | Auto-detection, validation, previews, custom styling |
| ReactTablePlugin | Table support | Insert tables, edit cells, add/remove rows/columns, i18n |
| ReactHRPlugin | Horizontal rules | Divider insertion, custom styling, markdown shortcuts |
| ReactFilePlugin | File attachments | File upload, status tracking, validation, drag-drop |
All plugins follow a dual-architecture design:
- Plugin Interface: Standardized plugin system with lifecycle management
- Service Container: Centralized service registration and dependency injection
- Command System: Event-driven command pattern for editor operations
- Node System: Custom node types with serialization and transformation
- Data Sources: Content management and format conversion (JSON, Markdown, Text)
- React Components: High-level components for easy integration
- Hook Integration: Custom hooks for editor state and functionality
- Event Handling: React-friendly event system and callbacks
- UI Components: Pre-built UI elements with theming support
- ✅ Fully configurable with TypeScript-typed options
- ✅ Composable - use any combination together
- ✅ Extensible - create custom plugins using the same API
- ✅ Event-driven - react to user interactions and content changes
- ✅ Service-oriented - modular architecture with dependency injection
- ✅ Internationalization - Built-in i18n support where applicable
- ✅ Markdown integration - Shortcuts, import/export, custom transformers
- ✅ Theme system - Customizable styling and appearance
- ✅ Command pattern - Programmatic control and automation
// Get editor instance
const editor = useEditor();
// Helper for plugin configuration
const PluginWithConfig = Editor.withProps(ReactFilePlugin, {
handleUpload: async (file) => ({ url: 'uploaded-url' }),
});Create a new editor kernel instance:
const editor = createEditor();Core editor methods:
interface IEditor {
// Content management
setDocument(type: string, content: any): void;
getDocument(type: string): any;
// Plugin system
registerPlugin<T>(plugin: Constructor<T>, config?: T): IEditor;
registerPlugins(plugins: Plugin[]): IEditor;
// Commands
dispatchCommand<T>(command: LexicalCommand<T>, payload: T): boolean;
// Events
on<T>(event: string, listener: (data: T) => void): this;
off<T>(event: string, listener: (data: T) => void): this;
// Lifecycle
focus(): void;
blur(): void;
destroy(): void;
// Access
getLexicalEditor(): LexicalEditor | null;
getRootElement(): HTMLElement | null;
requireService<T>(serviceId: ServiceID<T>): T | null;
}Each content node's durable public ID is stored at $.properties.nodeId; LiteXML exposes the same value as its id attribute. These IDs are opaque and independent of Lexical runtime keys. Newly generated IDs use 10 lowercase base-36 characters, with cryptographic randomness when the runtime provides it. Existing imported IDs remain unchanged unless they are invalid or collide with another content node. Read IDs from an exported LiteXML snapshot before using them as an operation's id, beforeId, or afterId. Importing JSON with keepId: true preserves valid, nonconflicting IDs; keepId: false assigns fresh IDs. The literal root is reserved for document-boundary beforeId and afterId insertion anchors. Imported content IDs that trim to root are repaired with fresh IDs.
Yjs v1 reconciles collisions created when disconnected clients import different nodes with the same explicit ID. Valid LiteXML review before/after pairs stay together; otherwise, the node with the smallest shared Yjs identity in deterministic order keeps the ID. Other nodes receive the same deterministic repair IDs on every client, and the repair is excluded from user undo history.
An ID names a logical content node, not one physical Lexical object. A pending LiteXML modification can contain before and after representations with the same ID. Lookup addresses the active after representation; pending removals are absent from the active view. Accepting or rejecting a change keeps the surviving logical ID, while an independent copy or split receives a new ID. Review wrappers and the document root are not addressable. Without LitexmlPlugin, CommonPlugin uses a plain tree view with no review rules.
Yjs external snapshots repair duplicate explicit IDs deterministically. Replaying the same malformed snapshot therefore keeps the repaired IDs and does not rewrite the shared document again.
Use $getNodeById only synchronously inside a Lexical read or update. It returns a Lexical node in that context, so do not retain the result across updates. A historical EditorState.read must receive { editor: lexicalEditor } to select that editor's identity policy. Invalid or unknown IDs return null.
For application code outside a Lexical callback, CommonPlugin registers INodeIdentityService. Its getNodeById returns detached, immutable { id, type, textContent } metadata from the last committed state. textContent is the native Lexical text for that node; an outer review container can include both pending sides. Use a LiteXML or Markdown export when you need the projected document text. subscribe signals committed updates and identity-policy changes; call getNodeById in the listener to refresh the metadata. Unsubscribe when the consumer unmounts. After the editor is destroyed, a retained service returns null.
The same $getNodeById, $getNodeId, and INodeIdentityService exports are available from @lobehub/editor/headless; headless consumers can use that entry without importing the browser bundle.
$getNodeById walks the active tree in O(N) time so pending edits and historical reads use the exact Lexical state in the callback. The service builds a key-only index once per committed EditorState in O(N) time, then resolves later IDs in O(1) before extracting native textContent (which can itself traverse a subtree). The identity benchmark records diagnostic timings; it does not set CI timing thresholds.
import { $getNodeById, INodeIdentityService, type IEditor } from '@lobehub/editor';
function inspectNode(kernel: IEditor, id: string) {
// CommonPlugin must be registered and the editor initialized.
const identity = kernel.requireService(INodeIdentityService);
const snapshot = identity?.getNodeById(id); // Safe to keep outside Lexical callbacks.
const lexicalEditor = kernel.getLexicalEditor();
lexicalEditor?.getEditorState().read(
() => {
const node = $getNodeById(id); // Active LiteXML representation, if installed.
console.log(node?.getType());
},
{ editor: lexicalEditor },
);
return snapshot;
}HeadlessEditor.applyLiteXMLBatchWithResults(operations) returns one result per input operation, in array order. status: 'applied' means the change is staged in the editor; pending review diffs still need a separate accept or reject action. A modify operation containing multiple LiteXML fragments applies all of its targets or none: missing targets, malformed fragments, incompatible list, table, or inline/block structure, repeated target IDs, and ancestor/descendant target pairs fail that operation before it changes the document. Distinct sibling targets can be modified together. Failed operations include a reason, and later independent operations still run:
import { createHeadlessEditor } from '@lobehub/editor/headless';
const editor = createHeadlessEditor();
editor.hydrateMarkdown('- first\n- second');
const { litexml } = editor.export({ litexml: true });
const targetId = /<li id="([^"]+)"/.exec(litexml!)?.[1];
if (!targetId) throw new Error('No list item ID found in LiteXML');
const operations = [
{ action: 'remove' as const, id: targetId },
{ action: 'remove' as const, id: 'unknown-id' },
];
const results = await editor.applyLiteXMLBatchWithResults(operations);
// results[1] has status: 'failed' and a reason.
editor.destroy();For a direct IEditor integration, dispatch LITEXML_MODIFY_WITH_RESULTS_COMMAND instead of calling the headless wrapper:
import type { IEditor } from '@lobehub/editor';
import { LITEXML_MODIFY_WITH_RESULTS_COMMAND } from '@lobehub/editor/litexml-commands';
function applyWithResults(kernel: IEditor) {
// The editor is already initialized with LiteXML and has a loaded document.
const currentLiteXml = kernel.getDocument('litexml') as unknown as string;
const targetId = /<li id="([^"]+)"/.exec(currentLiteXml)?.[1];
if (!targetId) throw new Error('No list item ID found in LiteXML');
kernel.dispatchCommand(LITEXML_MODIFY_WITH_RESULTS_COMMAND, {
operations: [
{ action: 'remove', id: targetId },
{ action: 'remove', id: 'unknown-id' },
],
onResults: (batchResults) => {
const failed = batchResults.filter((result) => result.status === 'failed');
console.log(failed);
},
});
}import { IEditorKernel, IEditorPlugin } from '@lobehub/editor';
class MyCustomPlugin implements IEditorPlugin {
constructor(private config: MyPluginConfig) {}
initialize(kernel: IEditorKernel) {
// Register nodes, commands, transforms, etc.
kernel.registerNode(MyCustomNode);
kernel.registerCommand(MY_COMMAND, this.handleCommand);
}
destroy() {
// Cleanup
}
}Common commands you can dispatch:
// Content insertion
INSERT_HEADING_COMMAND; // { tag: 'h1' | 'h2' | 'h3' }
INSERT_LINK_COMMAND; // { url: string, text?: string }
INSERT_IMAGE_COMMAND; // { src: string, alt?: string }
INSERT_TABLE_COMMAND; // { rows: number, columns: number }
INSERT_MENTION_COMMAND; // { label: string, extra?: any }
INSERT_FILE_COMMAND; // { file: File }
INSERT_HORIZONTAL_RULE_COMMAND;
// Text formatting
FORMAT_TEXT_COMMAND; // { format: 'bold' | 'italic' | 'underline' }
CLEAR_FORMAT_COMMAND;[![]back-to-top]
You can use Github Codespaces for online development:
Or clone it for local development:
$ git clone https://github.com/lobehub/lobe-editor.git
$ cd lobe-editor
$ pnpm install
$ pnpm run devThis starts the lobedocs development server with the live component playground. The local URL is printed in the terminal.
| Script | Description |
|---|---|
pnpm dev |
Start lobedocs with the component playground |
pnpm build |
Build library and generate type definitions |
pnpm test |
Run tests with Vitest |
pnpm test:coverage |
Run tests with coverage report |
pnpm lint |
Lint and fix code with ESLint |
pnpm type-check |
Type check with TypeScript |
pnpm ci |
Run all CI checks (lint, type-check, test) |
pnpm docs:build |
Build documentation for production |
pnpm release |
Publish new version with semantic-release |
LobeHub Editor includes comprehensive debug logging that can be controlled via environment variables:
# Enable all LobeHub Editor debug output
DEBUG=lobe-editor:*
# Enable only important logs (recommended for development)
DEBUG=lobe-editor:*:info,lobe-editor:*:warn,lobe-editor:*:error
# Enable specific components
DEBUG=lobe-editor:kernel,lobe-editor:plugin:*| Category | Description | Example |
|---|---|---|
kernel |
Core editor functionality | DEBUG=lobe-editor:kernel |
plugin:* |
All plugins | DEBUG=lobe-editor:plugin:* |
plugin:slash |
Slash commands | DEBUG=lobe-editor:plugin:slash |
plugin:mention |
Mention system | DEBUG=lobe-editor:plugin:mention |
plugin:image |
Image handling | DEBUG=lobe-editor:plugin:image |
plugin:file |
File operations | DEBUG=lobe-editor:plugin:file |
service:* |
All services | DEBUG=lobe-editor:service:* |
service:upload |
Upload service | DEBUG=lobe-editor:service:upload |
service:markdown |
Markdown processing | DEBUG=lobe-editor:service:markdown |
| Level | Browser Display | Usage | Environment Variable |
|---|---|---|---|
debug |
Console.log (gray) | Detailed tracing | DEBUG=lobe-editor:*:debug |
info |
Console.log (blue) | General information | DEBUG=lobe-editor:*:info |
warn |
Console.warn (yellow) | Warnings | DEBUG=lobe-editor:*:warn |
error |
Console.error (red) | Errors | DEBUG=lobe-editor:*:error |
# Full debug during development
DEBUG=lobe-editor:*
# Only critical logs
DEBUG=lobe-editor:*:error,lobe-editor:*:warn
# Plugin debugging
DEBUG=lobe-editor:plugin:*
# Service debugging
DEBUG=lobe-editor:service:*lobe-editor/
├── src/
│ ├── editor-kernel/ # 🧠 Core editor logic
│ │ ├── kernel.ts # Main editor class with plugin system
│ │ ├── data-source.ts # Content management (JSON/Markdown/Text)
│ │ ├── service.ts # Service container and dependency injection
│ │ ├── plugin/ # Plugin base classes and interfaces
│ │ ├── react/ # React integration layer
│ │ └── types.ts # TypeScript interfaces
│ │
│ ├── plugins/ # 🔌 Feature plugins
│ │ ├── common/ # 🏗️ Foundation components
│ │ │ ├── plugin/ # Base editor plugin
│ │ │ ├── react/ # ReactEditor, ReactEditorContent, ReactPlainText
│ │ │ ├── data-source/ # Content data sources
│ │ │ └── utils/ # Common utilities
│ │ │
│ │ ├── markdown/ # 📝 Markdown processing engine
│ │ │ ├── plugin/ # Markdown transformation plugin
│ │ │ ├── service/ # Markdown shortcut service
│ │ │ ├── data-source/ # Markdown serialization
│ │ │ └── utils/ # Transformer utilities
│ │ │
│ │ ├── upload/ # 📤 Upload management system
│ │ │ ├── plugin/ # Upload handling plugin
│ │ │ ├── service/ # Upload service with priority system
│ │ │ └── utils/ # Upload utilities
│ │ │
│ │ ├── slash/ # ⚡ Slash commands (/, @)
│ │ │ ├── plugin/ # Slash detection plugin
│ │ │ ├── react/ # ReactSlashPlugin, ReactSlashOption
│ │ │ ├── service/ # Slash service with fuzzy search
│ │ │ └── utils/ # Search and trigger utilities
│ │ │
│ │ ├── mention/ # 👤 @mention system
│ │ │ ├── plugin/ # Mention plugin with decorators
│ │ │ ├── react/ # ReactMentionPlugin
│ │ │ ├── command/ # INSERT_MENTION_COMMAND
│ │ │ └── node/ # MentionNode with serialization
│ │ │
│ │ ├── codeblock/ # 🎨 Syntax highlighting
│ │ │ ├── plugin/ # Codeblock plugin with Shiki
│ │ │ ├── react/ # ReactCodeblockPlugin
│ │ │ ├── command/ # Language and color commands
│ │ │ └── utils/ # Language detection
│ │ │
│ │ ├── image/ # 🖼️ Image upload & display
│ │ │ ├── plugin/ # Image plugin with captions
│ │ │ ├── react/ # ReactImagePlugin
│ │ │ ├── command/ # INSERT_IMAGE_COMMAND
│ │ │ └── node/ # BaseImageNode, ImageNode
│ │ │
│ │ ├── table/ # 📊 Table support
│ │ │ ├── plugin/ # Table plugin with i18n
│ │ │ ├── react/ # ReactTablePlugin
│ │ │ ├── command/ # Table manipulation commands
│ │ │ ├── node/ # Enhanced TableNode
│ │ │ └── utils/ # Table operations
│ │ │
│ │ ├── file/ # 📎 File attachments
│ │ │ ├── plugin/ # File plugin with status tracking
│ │ │ ├── react/ # ReactFilePlugin
│ │ │ ├── command/ # INSERT_FILE_COMMAND
│ │ │ ├── node/ # FileNode with metadata
│ │ │ └── utils/ # File operations
│ │ │
│ │ ├── link/ # 🔗 Link management
│ │ │ ├── plugin/ # Link plugin with validation
│ │ │ ├── react/ # ReactLinkPlugin
│ │ │ ├── command/ # Link commands
│ │ │ └── utils/ # URL validation and detection
│ │ │
│ │ ├── list/ # 📋 Lists (ordered/unordered)
│ │ │ ├── plugin/ # List plugin with nesting
│ │ │ ├── react/ # ReactListPlugin
│ │ │ ├── command/ # List manipulation commands
│ │ │ └── utils/ # List operations
│ │ │
│ │ └── hr/ # ➖ Horizontal rules
│ │ ├── plugin/ # HR plugin with styling
│ │ ├── react/ # ReactHRPlugin
│ │ ├── command/ # HR insertion commands
│ │ └── node/ # HorizontalRuleNode
│ │
│ ├── react/ # ⚛️ High-level React components
│ │ ├── Editor/ # Main Editor component with plugins
│ │ ├── ChatInput/ # Chat interface component
│ │ ├── ChatInputActions/ # Chat action buttons
│ │ ├── ChatInputActionBar/ # Action bar layout
│ │ ├── SendButton/ # Send button with states
│ │ └── CodeLanguageSelect/ # Code language selector
│ │
│ └── index.ts # Public API exports
│
├── docs/ # 📚 Documentation source
├── docs.config.ts # lobedocs site configuration
├── tests/ # 🧪 Test files
└── vitest.config.ts # Test configuration
The architecture follows a dual-layer design:
- Kernel Layer (
editor-kernel/) - Framework-agnostic core with plugin system - React Layer (
react/+plugins/*/react/) - React-specific implementations
Each plugin follows a consistent structure:
plugin/- Core plugin logic and node definitionsreact/- React components and hooks (if applicable)command/- Editor commands and handlersservice/- Services and business logicnode/- Custom Lexical nodesutils/- Utility functions and helpers
This allows for maximum flexibility - you can use just the kernel for custom integrations, or the React components for rapid development.
Contributions of all types are more than welcome, if you are interested in contributing code, feel free to check out our GitHub Issues to get stuck in to show us what you're made of.
- 🤯 Lobe Chat - An open-source, extensible (Function Calling), high-performance chatbot framework. It supports one-click free deployment of your private ChatGPT/LLM web application.
🅰️ Lobe Theme - The modern theme for stable diffusion webui, exquisite interface design, highly customizable UI, and efficiency boosting features.- 🧸 Lobe Vidol - Experience the magic of virtual idol creation with Lobe Vidol, enjoy the elegance of our Exquisite UI Design, dance along using MMD Dance Support, and engage in Smooth Conversations.
- 🍭 Lobe UI - An open-source UI component library for building AIGC web apps.
- 🥨 Lobe Icons - Popular AI / LLM Model Brand SVG Logo and Icon Collection.
- 📊 Lobe Charts - React modern charts components built on recharts
- 🎤 Lobe TTS - A high-quality & reliable TTS/STT library for Server and Browser
- 🌏 Lobe i18n - Automation ai tool for the i18n (internationalization) translation process.
