A lightweight, extensible Rich Text Editor for React -- zero extra dependencies beyond React itself.
npm install @overlap/rte- Lightweight -- only peer-depends on React, no heavy framework
- Plugin system -- compose exactly the toolbar you need
- Settings object -- configure features with a single object instead of assembling plugins manually
- Contenteditable-based -- uses native browser editing for performance
- Full formatting -- bold, italic, underline, strikethrough, subscript, superscript, inline code
- Block formats -- headings (h1-h6), bullet lists, numbered lists, checkbox lists, blockquote, code block
- Tables -- insert, row/column operations, context menu
- Images -- upload via callback, URL insert, drag & drop, paste
- Links -- floating editor, advanced attributes, custom fields
- Colors -- text color and background color pickers
- Font sizes -- configurable size dropdown
- Alignment -- left, center, right, justify
- Indent / Outdent -- nested list support via Tab / Shift+Tab
- Undo / Redo -- full history management
- HTML import/export --
htmlToContent()andcontentToHTML() - Theming -- CSS variables + theme prop for brand colors
- Lexical compatible -- correctly parses and renders HTML generated by Lexical editors
- TypeScript -- fully typed, ships
.d.ts
import { Editor, EditorContent } from "@overlap/rte";
import "@overlap/rte/dist/styles.css";
function App() {
const [content, setContent] = useState<EditorContent>();
return (
<Editor
initialContent={content}
onChange={setContent}
placeholder="Enter text..."
/>
);
}Note: Always import the CSS:
import '@overlap/rte/dist/styles.css'
Instead of assembling plugins manually, pass a settings object to control which features are enabled. This is the recommended approach for most use cases.
import { Editor, EditorSettings, buildPluginsFromSettings } from "@overlap/rte";
import "@overlap/rte/dist/styles.css";
const settings: EditorSettings = {
format: {
bold: true,
italic: true,
underline: true,
strikethrough: true,
code: false,
subscript: true,
superscript: true,
bulletList: true,
numberedList: true,
quote: true,
codeBlock: false,
check: true,
typography: ["h1", "h2", "h3"],
colors: ["#000", "#ff0000", "#00aa00", "#0000ff"],
fontSize: true,
alignment: ["left", "center", "right", "justify", "indent", "outdent"],
},
link: {
external: true,
internal: true,
},
table: {
enabled: true,
},
image: {
enabled: true,
},
};
function App() {
// Option A: Let the Editor build plugins from settings automatically
return <Editor settings={settings} onImageUpload={handleUpload} />;
// Option B: Build plugins yourself for more control
const plugins = buildPluginsFromSettings(settings, {
onImageUpload: handleUpload,
linkCustomFields: [
{
key: "urlExtra",
label: "Anchor / Params",
placeholder: "?param=value or #anchor",
dataAttribute: "data-url-extra",
appendToHref: true,
},
],
});
return <Editor plugins={plugins} />;
}interface EditorSettings {
format?: {
bold?: boolean;
italic?: boolean;
underline?: boolean;
strikethrough?: boolean;
code?: boolean; // inline code
subscript?: boolean;
superscript?: boolean;
bulletList?: boolean;
numberedList?: boolean;
quote?: boolean;
codeBlock?: boolean;
check?: boolean; // checkbox lists
typography?: string[]; // e.g. ["h1", "h2", "h3"]
colors?: string[]; // e.g. ["#000", "#ff0000"]
fontSize?: boolean;
alignment?: string[]; // e.g. ["left", "center", "right", "justify", "indent", "outdent"]
};
link?: {
external?: boolean;
internal?: boolean;
};
table?: {
enabled?: boolean;
};
image?: {
enabled?: boolean;
};
}By default, everything is enabled (defaultEditorSettings). Disable features by setting them to false or removing values from arrays.
For full control, assemble plugins yourself:
import {
Editor,
boldPlugin,
italicPlugin,
underlinePlugin,
strikethroughPlugin,
subscriptPlugin,
superscriptPlugin,
codeInlinePlugin,
undoPlugin,
redoPlugin,
clearFormattingPlugin,
indentListItemPlugin,
outdentListItemPlugin,
createBlockFormatPlugin,
createTextColorPlugin,
createBackgroundColorPlugin,
createFontSizePlugin,
createAlignmentPlugin,
createAdvancedLinkPlugin,
createImagePlugin,
tablePlugin,
} from "@overlap/rte";
const plugins = [
undoPlugin,
redoPlugin,
createBlockFormatPlugin(["h1", "h2", "h3"], {
bulletList: true,
numberedList: true,
quote: true,
check: true,
codeBlock: false,
}),
boldPlugin,
italicPlugin,
underlinePlugin,
strikethroughPlugin,
createAdvancedLinkPlugin({ enableTarget: true }),
createTextColorPlugin(["#000", "#ff0000", "#00aa00", "#0000ff"]),
createBackgroundColorPlugin(["#ffff00", "#00ff00", "#ff00ff"]),
createFontSizePlugin([12, 14, 16, 18, 20, 24, 28, 32]),
createAlignmentPlugin(["left", "center", "right", "justify"]),
tablePlugin,
createImagePlugin(handleUpload),
subscriptPlugin,
superscriptPlugin,
codeInlinePlugin,
clearFormattingPlugin,
indentListItemPlugin,
outdentListItemPlugin,
];
function App() {
return <Editor plugins={plugins} onChange={console.log} />;
}The link plugin supports a floating editor with advanced attributes and extensible custom fields:
import { createAdvancedLinkPlugin, LinkCustomField } from "@overlap/rte";
const linkPlugin = createAdvancedLinkPlugin({
enableTarget: true, // show "Open in new tab" checkbox
customFields: [
{
key: "urlExtra",
label: "Anchor / Params",
placeholder: "?param=value or #anchor",
dataAttribute: "data-url-extra",
appendToHref: true,
},
{
key: "pageRef",
label: "Page Reference",
placeholder: "Select a page...",
dataAttribute: "data-page-ref",
disablesUrl: true,
},
],
});| Property | Type | Description |
|---|---|---|
key |
string |
Unique identifier |
label |
string |
Input label text |
placeholder |
string? |
Input placeholder |
dataAttribute |
string |
HTML data attribute stored on the <a> tag |
appendToHref |
boolean? |
If true, value is appended to the href |
disablesUrl |
boolean? |
If true, hides the URL field when this field has a value |
import { Editor, createImagePlugin } from "@overlap/rte";
async function handleUpload(file: File): Promise<string> {
const formData = new FormData();
formData.append("file", file);
const res = await fetch("/api/upload", { method: "POST", body: formData });
const { url } = await res.json();
return url;
}
// Via settings:
<Editor settings={{ image: { enabled: true } }} onImageUpload={handleUpload} />
// Via plugin:
<Editor plugins={[...otherPlugins, createImagePlugin(handleUpload)]} />Images can be inserted by:
- Clicking the toolbar button (file picker or URL input)
- Pasting an image from the clipboard
- Dragging and dropping an image file
import { Editor, tablePlugin } from "@overlap/rte";
// Via settings:
<Editor settings={{ table: { enabled: true } }} />
// Via plugin:
<Editor plugins={[...otherPlugins, tablePlugin]} />Right-click a table cell to access the context menu with row/column operations.
Override CSS variables on .rte-container or a parent element:
.my-editor {
--rte-primary-color: #339192;
--rte-primary-light: rgba(51, 145, 146, 0.15);
--rte-border-color: #e0e0e0;
--rte-toolbar-bg: #fafafa;
--rte-content-bg: #ffffff;
--rte-button-hover-bg: rgba(51, 145, 146, 0.08);
--rte-border-radius: 8px;
}<Editor
theme={{
primaryColor: "#339192",
borderColor: "#e0e0e0",
borderRadius: 8,
toolbarBg: "#fafafa",
contentBg: "#ffffff",
buttonHoverBg: "rgba(51, 145, 146, 0.08)",
}}
/>.rte-container { /* outer wrapper */ }
.rte-toolbar { /* toolbar row */ }
.rte-toolbar-button { /* individual toolbar button */ }
.rte-toolbar-button-active { /* active state */ }
.rte-editor { /* contenteditable area */ }import { htmlToContent, contentToHTML, EditorAPI } from "@overlap/rte";
// Import HTML into the editor
const content = htmlToContent("<p>Hello <strong>world</strong></p>");
<Editor initialContent={content} />
// Export HTML from the editor
function MyEditor() {
const apiRef = useRef<EditorAPI>(null);
return (
<Editor
onEditorAPIReady={(api) => { apiRef.current = api; }}
onChange={() => {
const html = apiRef.current?.exportHtml();
console.log(html);
}}
/>
);
}| Prop | Type | Description |
|---|---|---|
initialContent |
EditorContent? |
Initial editor content (JSON) |
onChange |
(content: EditorContent) => void |
Called on every content change |
plugins |
Plugin[]? |
Manual plugin array (overrides settings) |
settings |
EditorSettings? |
Settings object to auto-build plugins |
settingsOptions |
BuildPluginsOptions? |
Extra options when using settings (e.g. onImageUpload, linkCustomFields) |
placeholder |
string? |
Placeholder text (default: "Enter text...") |
className |
string? |
CSS class for the outer container |
toolbarClassName |
string? |
CSS class for the toolbar |
editorClassName |
string? |
CSS class for the editor area |
headings |
string[]? |
Heading levels when using default plugins |
fontSizes |
number[]? |
Font sizes when using default plugins |
colors |
string[]? |
Color palette when using default plugins |
onImageUpload |
(file: File) => Promise<string> |
Image upload callback |
onEditorAPIReady |
(api: EditorAPI) => void |
Callback when editor API is ready |
theme |
object? |
Theme overrides (see Theming section) |
Available via onEditorAPIReady callback or passed to plugin functions:
| Method | Description |
|---|---|
executeCommand(cmd, value?) |
Execute a document.execCommand |
getSelection() |
Get the current Selection |
getContent() |
Get content as EditorContent JSON |
setContent(content) |
Set content from EditorContent JSON |
exportHtml() |
Export current content as HTML string |
importHtml(html) |
Import HTML string into the editor |
insertBlock(type, attrs?) |
Insert a block element |
insertInline(type, attrs?) |
Insert an inline element |
undo() / redo() |
History navigation |
canUndo() / canRedo() |
Check history state |
indentListItem() |
Indent the current list item |
outdentListItem() |
Outdent the current list item |
clearFormatting() |
Remove all formatting from selection |
clearTextColor() |
Remove text color |
clearBackgroundColor() |
Remove background color |
clearFontSize() |
Remove font size |
clearLinks() |
Remove links (keep text) |
import { Plugin, EditorAPI, ButtonProps } from "@overlap/rte";
const highlightPlugin: Plugin = {
name: "highlight",
type: "inline",
renderButton: (props: ButtonProps) => (
<button
onClick={props.onClick}
className={`rte-toolbar-button ${props.isActive ? "rte-toolbar-button-active" : ""}`}
title="Highlight"
>
H
</button>
),
execute: (editor: EditorAPI) => {
editor.executeCommand("backColor", "#ffff00");
},
isActive: () => {
const sel = document.getSelection();
if (!sel || sel.rangeCount === 0) return false;
const el = sel.getRangeAt(0).commonAncestorContainer;
const parent = el.nodeType === Node.TEXT_NODE ? el.parentElement : el as HTMLElement;
return parent?.closest("[style*='background-color: rgb(255, 255, 0)']") !== null;
},
canExecute: () => true,
};// Components
export { Editor, Toolbar, Dropdown };
// Settings
export { EditorSettings, defaultEditorSettings, buildPluginsFromSettings, BuildPluginsOptions };
// Individual plugins
export { boldPlugin, italicPlugin, underlinePlugin, strikethroughPlugin };
export { subscriptPlugin, superscriptPlugin, codeInlinePlugin };
export { undoPlugin, redoPlugin, clearFormattingPlugin };
export { indentListItemPlugin, outdentListItemPlugin };
export { defaultPlugins };
// Plugin factories
export { createBlockFormatPlugin, BlockFormatOptions };
export { createTextColorPlugin, createBackgroundColorPlugin };
export { createFontSizePlugin };
export { createAlignmentPlugin };
export { createAdvancedLinkPlugin, LinkCustomField };
export { createImagePlugin };
export { tablePlugin };
// Utilities
export { htmlToContent, contentToHTML, contentToDOM, domToContent, createEmptyContent };
export { HistoryManager };
// Types
export { Plugin, EditorAPI, EditorContent, EditorNode, EditorProps, ButtonProps };npm install # install dependencies
npm run build # production build
npm run dev # watch mode
cd example && npm run dev # run the showcase appMIT