Skip to content

Lightweight Rich Text Editor for React with plugin system and zero dependencies.

Notifications You must be signed in to change notification settings

overlap-dev/rte

Repository files navigation

@overlap/rte

A lightweight, extensible Rich Text Editor for React -- zero extra dependencies beyond React itself.

Installation

npm install @overlap/rte

Features

  • 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() and contentToHTML()
  • 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

Quick Start

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'


Settings-Based Configuration

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} />;
}

EditorSettings Interface

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.


Manual Plugin Composition

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} />;
}

Advanced Link Plugin

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,
        },
    ],
});

LinkCustomField

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

Image Upload

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

Tables

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.


Theming

CSS Variables

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;
}

Theme Prop

<Editor
    theme={{
        primaryColor: "#339192",
        borderColor: "#e0e0e0",
        borderRadius: 8,
        toolbarBg: "#fafafa",
        contentBg: "#ffffff",
        buttonHoverBg: "rgba(51, 145, 146, 0.08)",
    }}
/>

CSS Class Overrides

.rte-container        { /* outer wrapper */ }
.rte-toolbar          { /* toolbar row */ }
.rte-toolbar-button   { /* individual toolbar button */ }
.rte-toolbar-button-active { /* active state */ }
.rte-editor           { /* contenteditable area */ }

HTML Import / Export

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);
            }}
        />
    );
}

Editor Props

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)

EditorAPI

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)

Creating Custom Plugins

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,
};

Exports Overview

// 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 };

Development

npm install       # install dependencies
npm run build     # production build
npm run dev       # watch mode

cd example && npm run dev   # run the showcase app

License

MIT

About

Lightweight Rich Text Editor for React with plugin system and zero dependencies.

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •