npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@overlap/rte

v1.0.2

Published

A lightweight, extensible Rich Text Editor for React

Downloads

672

Readme

@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, horizontal rule
  • Tables -- insert, row/column operations, context menu
  • Images -- upload via callback, URL insert, drag & drop, paste, alt text support
  • Links -- floating editor, advanced attributes, custom fields, hover tooltip
  • Colors -- text color and background color pickers with custom hex input
  • Font sizes -- configurable size dropdown
  • Alignment -- left, center, right, justify
  • Indent / Outdent -- nested list support via Tab / Shift+Tab
  • Undo / Redo -- full history management with cursor restoration
  • HTML import/export -- htmlToContent() and contentToHTML()
  • Keyboard shortcuts -- Cmd/Ctrl+B, I, U, E, Shift+X, Shift+7, Shift+8 and more
  • Markdown shortcuts -- type #, -, >, [], ---, ``` to auto-format
  • Auto-linking -- URLs are auto-detected and converted to links on Space/Enter
  • Read-only mode -- readOnly prop disables editing and hides toolbars
  • Floating toolbar -- appears on text selection with formatting options
  • Word/character count -- optional showWordCount display
  • Max length -- enforce character limit with maxLength prop
  • Focus/blur callbacks -- onFocus / onBlur props
  • HTML sanitization -- built-in XSS protection on paste, import, and JSON rendering
  • 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) | | readOnly | boolean? | When true, hides toolbars and disables editing | | onFocus | () => void | Called when the editor receives focus | | onBlur | () => void | Called when the editor loses focus | | maxLength | number? | Maximum character count; prevents input beyond this limit | | showWordCount | boolean? | Shows a word/character count bar below the editor |


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) | | getTextStats() | Get { characters, words } count |


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 { horizontalRulePlugin };
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 };
export { sanitizeHtml, isUrlSafe };

// Types
export { Plugin, EditorAPI, EditorContent, EditorNode, EditorProps, ButtonProps };

Security

The editor includes built-in XSS protection:

  • HTML sanitization -- htmlToContent() automatically sanitizes incoming HTML, stripping <script>, <iframe>, event handlers (onclick, onerror, etc.), and javascript: URLs.
  • URL validation -- isUrlSafe(url) is available as an export to validate URLs against a safe scheme allowlist. It is used internally for links, images, and auto-linking.
  • Paste protection -- pasted HTML is sanitized before insertion.
  • Allowlist-based -- the sanitizer uses a tag and attribute allowlist approach, removing dangerous elements (<script>, <iframe>, <object>, <embed>, <form>, <template>, <video>, <audio>, <marquee>, etc.) and filtering attributes that start with on or contain unsafe values.
import { sanitizeHtml, isUrlSafe } from "@overlap/rte";

// Sanitize untrusted HTML
const clean = sanitizeHtml('<p>Hello</p><script>alert("xss")</script>');

// Validate a URL
if (isUrlSafe(userUrl)) {
    // safe to use
}

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