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

@remyxjs/react

v1.3.0-beta

Published

React components and hooks for the Remyx rich-text editor

Downloads

606

Readme

Remyx Editor

@remyxjs/react

A feature-rich WYSIWYG editor for React, built on the framework-agnostic @remyxjs/core engine. Configurable toolbar, menu bar, markdown support, theming, file uploads, and a plugin system.

Visit us at SmashJaw.com or the RemyxJS official site: Remyxjs.com

Packages

| Package | Version | Description | | -------------------------------------------------------------- | ---------- | ----------------------------------------------------------------------- | | @remyxjs/core | 1.3.0-beta | Framework-agnostic engine, commands, plugins, utilities, and CSS themes | | @remyxjs/react | 1.3.0-beta | React components, hooks, and TypeScript declarations |

Use @remyxjs/core directly if building a wrapper for another framework (Vue, Svelte, Angular, vanilla JS).

Table of Contents

Installation

npm install @remyxjs/core @remyxjs/react

Then scaffold the remyxjs/ directory in your project root:

npx remyxjs init

This creates remyxjs/config/, remyxjs/plugins/, and remyxjs/themes/ with built-in presets, plugins, and theme CSS files. The editor discovers these at runtime via Vite's import.meta.glob(). See the setup guide for details and CLI options (--force, --no-plugins, --no-themes).

Import both stylesheets in your app entry point:

import '@remyxjs/core/style.css'; // theme variables, light/dark themes
import '@remyxjs/react/style.css'; // component styles (toolbar, modals, etc.)

Quick Start

import { useState } from 'react';
import { RemyxEditor } from '@remyxjs/react';
import '@remyxjs/core/style.css';
import '@remyxjs/react/style.css';

function App() {
  const [content, setContent] = useState('');

  return (
    <RemyxEditor
      value={content}
      onChange={setContent}
      placeholder="Start typing..."
      height={400}
    />
  );
}

Uncontrolled Mode

<RemyxEditor
  defaultValue="<p>Initial content</p>"
  onChange={(html) => console.log(html)}
/>

Markdown Mode

const [markdown, setMarkdown] = useState(
  '# Hello\n\nStart typing...',
);

<RemyxEditor
  value={markdown}
  onChange={setMarkdown}
  outputFormat="markdown"
/>;

With Upload Handler

<RemyxEditor
  value={content}
  onChange={setContent}
  uploadHandler={async (file) => {
    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;
  }}
/>

Dark Theme with Google Fonts

<RemyxEditor
  theme="dark"
  googleFonts={['Inter', 'Fira Code', 'Merriweather']}
  height={500}
/>

Features

  • Rich text editing — bold, italic, underline, strikethrough, subscript, superscript, headings, lists, blockquotes, code blocks, tables (with sorting, filtering, formulas, cell formatting, resize handles, and clipboard interop), and horizontal rules
  • Toolbar — fully configurable with presets, helper functions, and per-item theming
  • Menu bar — application-style menus (File, Edit, View, Insert, Format) with submenus and auto-deduplication
  • Markdown — bidirectional HTML/Markdown conversion, toggle between modes, and auto-detection on paste
  • Theming — light/dark mode, 4 built-in presets (Ocean, Forest, Sunset, Rose), full CSS variable customization
  • File uploads — images and attachments via drag-and-drop, paste, or toolbar with pluggable upload handlers (S3, R2, GCS, custom)
  • Fonts — custom font lists, Google Fonts auto-loading, and helper functions
  • Code block syntax highlighting — 11 languages with auto-detection, theme-aware colors, language selector dropdown, line numbers toggle, copy-to-clipboard, inline code highlighting, and extensible language registry
  • Enhanced tables — sortable columns, multi-column sort, filterable rows, inline formulas, cell formatting, column/row resize, sticky headers, and Excel/Sheets clipboard interop via TablePlugin
  • Template system{{merge_tag}} chips, {{#if}}/{{#each}} blocks, live preview, 5 pre-built templates, custom template library via TemplatePlugin
  • Keyboard-first editing — Vim (normal/insert/visual), Emacs keybindings, custom key map, multi-cursor (Cmd+D), smart auto-pairing, jump-to-heading via KeyboardPlugin
  • Drag-and-drop content — Drop zones, cross-editor drag, file/image/rich-text drops, block reorder with ghost preview via DragDropPlugin
  • Advanced link management — Link previews, broken link detection, auto-linking, click analytics, bookmark anchors via LinkPlugin
  • Callouts & alerts — 7 built-in callout types with custom type registration, collapsible toggle, nested content, and GitHub-flavored alert syntax auto-conversion via CalloutPlugin
  • Comments & annotations — Inline comment threads via CommentsPlugin, CommentsPanel sidebar with thread cards/replies/actions, useComments hook for reactive state, @mention parsing, resolved/unresolved state, comment-only mode, import/export
  • Real-time collaboration — CRDT-based co-editing with live cursors, presence indicators, offline-first sync, configurable transport (WebSocket, WebRTC, or custom) via CollaborationPlugin, useCollaboration hook, CollaborationBar component
  • PluginscreatePlugin() API with hooks for commands, toolbar items, status bar items, and context menus
  • Config file — JSON config files in remyxjs/config/ with 5 built-in presets (default, minimal, blog-editor, full-toolbar, toolbar-and-menu)
  • Block-based editing — block-level toolbar with type conversion, drag-to-reorder, collapsible sections, block grouping, BlockTemplatePlugin with built-in templates
  • Mobile & touch — touch floating toolbar, swipe indent/outdent, long-press context menu with haptic feedback, pinch-to-zoom, responsive toolbar overflow, virtual keyboard-aware layout
  • Multi-instance — unlimited editors per page with full isolation (state, events, DOM, modals), EditorBus for inter-editor communication, SharedResources for memory-efficient shared schemas and icons
  • Import/Export — PDF, DOCX, Markdown, CSV, and HTML
  • Paste cleaning — intelligent cleanup from Word, Google Docs, LibreOffice, Pages, and raw markdown
  • Command palette — searchable overlay with all editor commands, opened via Mod+Shift+P or toolbar button
  • Keyboard shortcuts — customizable with sensible defaults
  • Accessibility — semantic HTML, ARIA attributes, keyboard navigation, and focus management

Props

| Prop | Type | Default | Description | | --------------------- | ---------------------------------------------------------------- | ------------- | ----------------------------------------------------------------------------------------- | | config | string | — | Config file name from remyxjs/config/ (e.g., 'default', 'minimal', 'blog-editor') | | value | string | — | Controlled content (HTML or Markdown) | | defaultValue | string | — | Initial content for uncontrolled mode | | onChange | (content: string) => void | — | Called when content changes | | outputFormat | 'html' \| 'markdown' | 'html' | Format passed to onChange | | toolbar | string[][] | Full toolbar | Custom toolbar layout | | menuBar | boolean \| MenuBarConfig[] | — | Enable menu bar | | theme | 'light' \| 'dark' \| 'ocean' \| 'forest' \| 'sunset' \| 'rose' | 'light' | Editor theme | | placeholder | string | '' | Placeholder text | | height | number | 300 | Editor height in px | | minHeight | number | — | Minimum height | | maxHeight | number | — | Maximum height (scrolls) | | readOnly | boolean | false | Disable editing | | fonts | string[] | Built-in list | Custom font families | | googleFonts | string[] | — | Google Font families to auto-load | | statusBar | 'bottom' \| 'top' \| 'popup' \| false | 'bottom' | Word/character count position | | customTheme | object | — | CSS variable overrides | | toolbarItemTheme | object | — | Per-item toolbar styling | | floatingToolbar | boolean | true | Show toolbar on text selection | | contextMenu | boolean | true | Show right-click context menu | | commandPalette | boolean | true | Enable command palette (Mod+Shift+P or toolbar button) | | autosave | boolean \| AutosaveConfig | false | Enable autosave with optional config (storage provider, interval, key) | | emptyState | boolean \| ReactNode | false | Show empty state when editor has no content (true for default, or custom React node) | | breadcrumb | boolean | false | Show breadcrumb bar with DOM path to current selection | | minimap | boolean | false | Show minimap preview on right edge | | splitViewFormat | 'html' \| 'markdown' | 'html' | Format for split view preview pane | | customizableToolbar | boolean | false | Enable drag-and-drop toolbar button rearrangement | | onToolbarChange | (order: string[]) => void | — | Called when toolbar order changes via drag | | plugins | Plugin[] | — | Custom plugins | | uploadHandler | (file: File) => Promise<string> | — | File upload handler | | shortcuts | object | — | Keyboard shortcut overrides | | sanitize | object | — | HTML sanitization options | | attachTo | React.RefObject | — | Attach to an existing element | | onReady | (engine) => void | — | Called when editor initializes | | onFocus | () => void | — | Called on focus | | onBlur | () => void | — | Called on blur | | className | string | '' | CSS class for wrapper | | style | object | — | Inline styles for wrapper |

Config File

Each <RemyxEditor /> instance is driven by a JSON config file in remyxjs/config/. The config prop references the file name (without .json).

Built-in Presets

5 config presets ship out of the box:

<RemyxEditor config="default" />          {/* Full-featured editor */}
<RemyxEditor config="minimal" />          {/* Stripped-down toolbar */}
<RemyxEditor config="blog-editor" />      {/* Blog-focused layout */}
<RemyxEditor config="full-toolbar" />     {/* All toolbar items */}
<RemyxEditor config="toolbar-and-menu" /> {/* Toolbar + menu bar */}

Custom Config

Create a JSON file in remyxjs/config/ (e.g., remyxjs/config/my-editor.json):

{
  "theme": "dark",
  "placeholder": "Start writing...",
  "height": 400,
  "toolbar": [
    ["bold", "italic", "underline"],
    ["link", "image"]
  ],
  "plugins": {
    "syntax-highlight": true,
    "table": true
  }
}

Then use it:

<RemyxEditor config="my-editor" />

Config Resolution Priority

Values merge with this priority (highest first):

  1. Component props — always win (e.g., <RemyxEditor config="default" theme="dark" />)
  2. Config file — values from the JSON file in remyxjs/config/
  3. Built-in defaultstheme: 'light', height: 300, etc.

Toolbar

Default Toolbar

[
  ['undo', 'redo'],
  ['headings', 'fontFamily', 'fontSize'],
  ['bold', 'italic', 'underline', 'strikethrough'],
  ['foreColor', 'backColor'],
  ['alignLeft', 'alignCenter', 'alignRight', 'alignJustify'],
  ['orderedList', 'unorderedList', 'taskList'],
  ['outdent', 'indent'],
  [
    'link',
    'image',
    'table',
    'embedMedia',
    'blockquote',
    'codeBlock',
    'horizontalRule',
  ],
  ['subscript', 'superscript'],
  [
    'findReplace',
    'toggleMarkdown',
    'sourceMode',
    'export',
    'fullscreen',
  ],
];

Each inner array is a visual group separated by a divider.

Custom Toolbar via Props

<RemyxEditor
  toolbar={[
    ['bold', 'italic', 'underline'],
    ['orderedList', 'unorderedList'],
    ['link', 'image'],
  ]}
/>

Toolbar Presets

import { TOOLBAR_PRESETS } from '@remyxjs/react';

<RemyxEditor toolbar={TOOLBAR_PRESETS.full} />      // all items including plugin commands (default)
<RemyxEditor toolbar={TOOLBAR_PRESETS.rich} />       // all features with callout, math, toc, bookmark, merge tag
<RemyxEditor toolbar={TOOLBAR_PRESETS.standard} />   // common editing without plugins
<RemyxEditor toolbar={TOOLBAR_PRESETS.minimal} />    // headings, basics, lists, links, images
<RemyxEditor toolbar={TOOLBAR_PRESETS.bare} />       // bold, italic, underline only

Toolbar Helper Functions

removeToolbarItems(config, itemsToRemove)

import { DEFAULT_TOOLBAR, removeToolbarItems } from '@remyxjs/react';

const toolbar = removeToolbarItems(DEFAULT_TOOLBAR, [
  'image',
  'table',
  'embedMedia',
  'export',
]);

addToolbarItems(config, items, options?)

import { TOOLBAR_PRESETS, addToolbarItems } from '@remyxjs/react';

addToolbarItems(TOOLBAR_PRESETS.minimal, ['fullscreen']); // append as new group
addToolbarItems(TOOLBAR_PRESETS.minimal, 'fullscreen', { group: -1 }); // add to last group
addToolbarItems(TOOLBAR_PRESETS.minimal, 'taskList', {
  after: 'unorderedList',
});
addToolbarItems(TOOLBAR_PRESETS.minimal, 'strikethrough', {
  before: 'underline',
});

createToolbar(items)

Build a toolbar from a flat list — items are auto-grouped by category:

import { createToolbar } from '@remyxjs/react';

const toolbar = createToolbar([
  'bold',
  'italic',
  'underline',
  'headings',
  'link',
  'image',
  'orderedList',
  'unorderedList',
  'fullscreen',
]);
// Groups by category: [['headings'], ['bold', 'italic', 'underline'], ['orderedList', 'unorderedList'], ['link', 'image'], ['fullscreen']]

Available Toolbar Items

| Item | Type | Description | | -------------------- | ------------ | -------------------------------------------------------------------- | | undo | Button | Undo last action | | redo | Button | Redo last action | | headings | Dropdown | Block type (Normal, H1–H6) | | fontFamily | Dropdown | Font family selector | | fontSize | Dropdown | Font size selector | | bold | Button | Bold | | italic | Button | Italic | | underline | Button | Underline | | strikethrough | Button | Strikethrough | | subscript | Button | Subscript | | superscript | Button | Superscript | | foreColor | Color Picker | Text color | | backColor | Color Picker | Background color | | alignLeft | Button | Align left | | alignCenter | Button | Align center | | alignRight | Button | Align right | | alignJustify | Button | Justify | | orderedList | Button | Numbered list | | unorderedList | Button | Bulleted list | | taskList | Button | Task/checkbox list | | indent | Button | Increase indent | | outdent | Button | Decrease indent | | link | Button | Insert/edit link | | image | Button | Insert image | | attachment | Button | Attach file | | importDocument | Button | Import (PDF, DOCX, MD, CSV) | | table | Button | Insert table | | embedMedia | Button | Embed video/media | | blockquote | Button | Block quote | | codeBlock | Button | Code block | | horizontalRule | Button | Horizontal divider | | findReplace | Button | Find and replace | | toggleMarkdown | Button | Toggle markdown mode | | sourceMode | Button | View/edit HTML source | | export | Button | Export (PDF, MD, DOCX) | | commandPalette | Button | Open command palette | | fullscreen | Button | Toggle fullscreen | | insertCallout | Button | Insert callout block | | insertMath | Button | Insert math equation | | insertToc | Button | Insert table of contents | | insertBookmark | Button | Insert bookmark anchor | | insertMergeTag | Button | Insert merge tag | | toggleAnalytics | Button | Toggle analytics panel | | toggleSpellcheck | Button | Toggle spellcheck | | checkGrammar | Button | Run grammar check | | addComment | Button | Add inline comment on selection | | removeFormat | Button | Remove all inline formatting | | distractionFree | Button | Toggle distraction-free mode (Mod+Shift+D) | | toggleSplitView | Button | Toggle side-by-side preview (Mod+Shift+V) | | typography | Dropdown | Typography controls (line height, letter spacing, paragraph spacing) | | lineHeight | Dropdown | Line height adjustment | | letterSpacing | Dropdown | Letter spacing adjustment | | paragraphSpacing | Dropdown | Paragraph spacing adjustment | | startCollaboration | Button | Start real-time collaboration session | | stopCollaboration | Button | Stop collaboration session |

Menu Bar

Add an application-style menu bar above the toolbar. Both the toolbar and menu bar display all their items — the menu bar is an additional navigation layer, not a replacement for the toolbar.

Enable

<RemyxEditor menuBar={true} />

Default menus:

| Menu | Items | | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | File | Import Document, Export Document | | Edit | Undo, Redo, Find & Replace | | View | Fullscreen, Distraction-Free Mode, Split View, Toggle Markdown, Source Mode, Toggle Analytics | | Insert | Link, Image, Table, Attachment, Embed Media, Blockquote, Code Block, Horizontal Rule, Insert Callout, Insert Math, Insert TOC, Insert Bookmark, Insert Merge Tag, Add Comment | | Format | Bold, Italic, Underline, Strikethrough, Subscript, Superscript, Alignment, Lists, Colors, Typography (Line Height, Letter Spacing, Paragraph Spacing), Remove Formatting |

Custom Menu Bar

<RemyxEditor
  menuBar={[
    { label: 'File', items: ['importDocument', 'export'] },
    { label: 'Edit', items: ['undo', 'redo', '---', 'findReplace'] },
    {
      label: 'Format',
      items: [
        'bold',
        'italic',
        'underline',
        '---',
        {
          label: 'Alignment',
          items: [
            'alignLeft',
            'alignCenter',
            'alignRight',
            'alignJustify',
          ],
        },
      ],
    },
  ]}
/>

Each menu object has a label (string) and items array of command names, '---' separators, or nested { label, items } submenu objects. Any command from the Available Toolbar Items table works as a menu item.

Menu Bar with Config File

In your config JSON (remyxjs/config/my-editor.json):

{
  "menuBar": [
    { "label": "Edit", "items": ["undo", "redo"] },
    { "label": "Format", "items": ["bold", "italic", "underline"] }
  ],
  "toolbar": [["headings", "fontFamily", "fontSize"]]
}

Toolbar and Menu Bar

When both the toolbar and menu bar are enabled, the full toolbar is preserved. The menu bar provides an additional navigation layer — it does not remove items from the toolbar. Both surfaces can be used together to give users maximum flexibility.

Menu Bar Behavior

  • Click a trigger to open, hover to switch between open menus
  • Escape or click outside to close
  • Submenus open on hover with a chevron
  • Toggle commands show active state, modal commands open their dialogs
  • Inherits the editor's theme and custom theme variables

Multiple Editors

Multiple <RemyxEditor /> instances work on the same page without any additional setup. Each editor is fully isolated.

Basic Usage

<RemyxEditor placeholder="Editor 1..." height={300} />
<RemyxEditor placeholder="Editor 2..." height={300} />
<RemyxEditor placeholder="Editor 3..." height={200} />

What's Isolated Per Instance

| Feature | Isolation | | ---------------------- | ----------------------------------------------------------------- | | Content & state | Separate undo/redo history, content, and selection | | Toolbar | Independent dropdowns, color pickers, and active states | | Menu bar | Menus open/close independently per editor | | Modals | Each editor has its own modals (link, image, table, find/replace) | | Floating toolbar | Appears only for the editor with an active selection | | Context menu | Scoped to the clicked editor | | Fullscreen | Each editor enters/exits fullscreen independently | | Plugins | Per-editor plugin instances with separate state | | Events | Separate EventBus per instance | | Keyboard shortcuts | Fire only for the focused editor |

Mixed Configurations

<RemyxEditor config="default" menuBar={true} height={400} />
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
  <RemyxEditor config="minimal" />
  <RemyxEditor config="blog-editor" />
</div>
<RemyxEditor config="default" theme="dark" placeholder="Standalone editor" />

No hard limit on editor count. Performance scales linearly. All runtime state is per-instance.

Font Configuration

Custom Font List

<RemyxEditor
  fonts={['Arial', 'Georgia', 'Courier New', 'My Custom Font']}
/>

Google Fonts

<RemyxEditor googleFonts={['Roboto', 'Open Sans', 'Lato', 'Montserrat']} />

// With custom fonts
<RemyxEditor fonts={['Arial', 'Georgia']} googleFonts={['Poppins', 'Inter']} />

// Specific weights
<RemyxEditor googleFonts={['Roboto:wght@400;700']} />

Google Fonts are auto-loaded via CDN and merged into the font dropdown.

Font Helper Functions

import {
  DEFAULT_FONTS,
  removeFonts,
  addFonts,
  loadGoogleFonts,
} from '@remyxjs/react';

// Remove fonts
const fonts = removeFonts(DEFAULT_FONTS, ['Comic Sans MS', 'Impact']);

// Add fonts (append by default, or { position: 'start' } to prepend)
const fonts = addFonts(DEFAULT_FONTS, ['My Custom Font']);

// Load Google Fonts programmatically
loadGoogleFonts(['Roboto', 'Lato']);

Status Bar

The status bar displays word and character counts.

<RemyxEditor statusBar="bottom" />  {/* below content (default) */}
<RemyxEditor statusBar="top" />     {/* above content */}
<RemyxEditor statusBar="popup" />   {/* toolbar button with popover */}
<RemyxEditor statusBar={false} />   {/* hidden */}

Theming

<RemyxEditor theme="light" />  // default
<RemyxEditor theme="dark" />

Custom Themes

Override CSS variables using createTheme() or raw variable names. Custom themes layer on top of the base theme — only specify what you want to change.

import { createTheme } from '@remyxjs/react';

const brandTheme = createTheme({
  bg: '#1a1a2e',
  text: '#e0e0e0',
  primary: '#e94560',
  primaryHover: '#c81e45',
  toolbarBg: '#16213e',
  toolbarIcon: '#94a3b8',
  toolbarIconActive: '#e94560',
  radius: '12px',
  contentFontSize: '18px',
});

<RemyxEditor theme="dark" customTheme={brandTheme} />;

Or pass raw CSS variables directly:

<RemyxEditor
  customTheme={{ '--rmx-primary': '#e94560', '--rmx-bg': '#1a1a2e' }}
/>

Theme Presets

All 6 built-in themes are available via the theme prop:

<RemyxEditor theme="light" />   {/* Default — clean white */}
<RemyxEditor theme="dark" />    {/* Neutral dark */}
<RemyxEditor theme="ocean" />   {/* Deep blue */}
<RemyxEditor theme="forest" />  {/* Green earth-tone */}
<RemyxEditor theme="sunset" />  {/* Warm orange/amber */}
<RemyxEditor theme="rose" />    {/* Soft pink */}

Override individual variables on top of any theme with customTheme:

import { createTheme } from '@remyxjs/react';

<RemyxEditor
  theme="ocean"
  customTheme={createTheme({ primary: '#ff6b6b' })}
/>;

Available Theme Variables

| Key | CSS Variable | Description | | --------------------- | ----------------------------- | --------------------------- | | bg | --rmx-bg | Editor background | | text | --rmx-text | Primary text color | | textSecondary | --rmx-text-secondary | Muted text color | | border | --rmx-border | Border color | | borderSubtle | --rmx-border-subtle | Subtle border color | | toolbarBg | --rmx-toolbar-bg | Toolbar background | | toolbarBorder | --rmx-toolbar-border | Toolbar border | | toolbarButtonHover | --rmx-toolbar-button-hover | Button hover background | | toolbarButtonActive | --rmx-toolbar-button-active | Button active background | | toolbarIcon | --rmx-toolbar-icon | Icon color | | toolbarIconActive | --rmx-toolbar-icon-active | Active icon color | | primary | --rmx-primary | Primary accent color | | primaryHover | --rmx-primary-hover | Primary hover color | | primaryLight | --rmx-primary-light | Light primary (backgrounds) | | focusRing | --rmx-focus-ring | Focus outline color | | selection | --rmx-selection | Text selection color | | danger | --rmx-danger | Error/danger color | | dangerLight | --rmx-danger-light | Light danger color | | placeholder | --rmx-placeholder | Placeholder text color | | modalBg | --rmx-modal-bg | Modal background | | modalOverlay | --rmx-modal-overlay | Modal overlay | | statusbarBg | --rmx-statusbar-bg | Status bar background | | statusbarText | --rmx-statusbar-text | Status bar text | | fontFamily | --rmx-font-family | UI font stack | | fontSize | --rmx-font-size | UI font size | | contentFontSize | --rmx-content-font-size | Content font size | | contentLineHeight | --rmx-content-line-height | Content line height | | radius | --rmx-radius | Border radius | | radiusSm | --rmx-radius-sm | Small border radius | | spacingXs | --rmx-spacing-xs | Extra small spacing | | spacingSm | --rmx-spacing-sm | Small spacing | | spacingMd | --rmx-spacing-md | Medium spacing |

Per-Item Toolbar Theming

Style individual toolbar buttons independently using toolbarItemTheme.

import { createToolbarItemTheme } from '@remyxjs/react';

const itemTheme = createToolbarItemTheme({
  bold: {
    color: '#e11d48',
    activeColor: '#be123c',
    activeBackground: '#ffe4e6',
    borderRadius: '50%',
  },
  italic: {
    color: '#7c3aed',
    activeColor: '#6d28d9',
    activeBackground: '#ede9fe',
  },
  underline: {
    color: '#0891b2',
    activeColor: '#0e7490',
    activeBackground: '#cffafe',
  },
  _separator: { color: '#c4b5fd', width: '2px' },
});

<RemyxEditor toolbarItemTheme={itemTheme} />;

Both customTheme and toolbarItemTheme can be used together — customTheme sets global styles, toolbarItemTheme overrides specific items.

Per-item style properties:

| Key | CSS Variable | Description | | ------------------ | ----------------------- | ---------------------- | | color | --rmx-tb-color | Icon/text color | | background | --rmx-tb-bg | Default background | | hoverColor | --rmx-tb-hover-color | Color on hover | | hoverBackground | --rmx-tb-hover-bg | Background on hover | | activeColor | --rmx-tb-active-color | Color when active | | activeBackground | --rmx-tb-active-bg | Background when active | | border | --rmx-tb-border | Border shorthand | | borderRadius | --rmx-tb-radius | Border radius | | size | --rmx-tb-size | Button width & height | | iconSize | --rmx-tb-icon-size | Icon size | | padding | --rmx-tb-padding | Button padding | | opacity | --rmx-tb-opacity | Button opacity |

Separator properties (via _separator key):

| Key | CSS Variable | Description | | -------- | --------------------- | ---------------- | | color | --rmx-tb-sep-color | Separator color | | width | --rmx-tb-sep-width | Separator width | | height | --rmx-tb-sep-height | Separator height | | margin | --rmx-tb-sep-margin | Separator margin |

Paste & Auto-Conversion

The editor cleans and normalizes pasted content automatically.

| Source | Handling | | ------------------ | ----------------------------------------------------------------------------------------------------------- | | Microsoft Word | Strips Office XML, mso-* styles, conditional comments. Converts Word-style lists to proper <ul>/<ol>. | | Google Docs | Removes internal IDs/classes. Converts styled spans to semantic tags (<strong>, <em>, <s>). | | LibreOffice | Strips namespace tags and auto-generated class names. | | Apple Pages | Removes iWork-specific attributes. | | Markdown | Auto-detects and converts to rich HTML (headings, lists, bold, links, code fences, etc.). | | Plain text | Wraps in <p> tags with <br> for line breaks. | | Images | Inserted as base64 data URIs, or uploaded via uploadHandler if configured. |

All paste paths (keyboard, drag-and-drop, context menu) share the same pipeline: HTML goes through cleanPastedHTML() then Sanitizer.sanitize(). Plain text is checked by looksLikeMarkdown() and converted if detected.

import { cleanPastedHTML, looksLikeMarkdown } from '@remyxjs/react';

const clean = cleanPastedHTML(dirtyHtml);
const isMarkdown = looksLikeMarkdown(text);

File Uploads

The uploadHandler prop controls where images and file attachments are stored. It receives a File and returns a Promise<string> with the file URL.

Without an uploadHandler, images are inserted as base64 data URIs and the attachment upload tab is disabled.

Custom Server

<RemyxEditor
  uploadHandler={async (file) => {
    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;
  }}
/>

S3 / R2 / GCS (Pre-signed URL)

<RemyxEditor
  uploadHandler={async (file) => {
    const res = await fetch('/api/presign', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        filename: file.name,
        contentType: file.type,
      }),
    });
    const { uploadUrl, publicUrl } = await res.json();
    await fetch(uploadUrl, {
      method: 'PUT',
      headers: { 'Content-Type': file.type },
      body: file,
    });
    return publicUrl;
  }}
/>

Upload Paths

The handler is called consistently across all upload surfaces:

| Path | Result | | ------------------------------- | -------------------------------------- | | Image toolbar (Upload tab) | Inserted as <img> | | Attachment toolbar (Upload tab) | Inserted as attachment chip | | Drag & drop image/file | Image as <img>, others as attachment | | Paste image/file | Same as drag & drop |

Output Formats

<RemyxEditor outputFormat="html" onChange={(html) => console.log(html)} />
<RemyxEditor outputFormat="markdown" onChange={(md) => console.log(md)} />

Conversion utilities for standalone use:

import { htmlToMarkdown, markdownToHtml } from '@remyxjs/react';

const md = htmlToMarkdown('<h1>Hello</h1><p>World</p>');
const html = markdownToHtml('# Hello\n\nWorld');

Attaching to Existing Elements

Enhance an existing <textarea> or <div> with a full WYSIWYG editor.

Textarea

const textareaRef = useRef(null);

<textarea ref={textareaRef} defaultValue="Initial content" />
<RemyxEditor attachTo={textareaRef} outputFormat="markdown" height={400} />

The textarea is hidden and its value stays in sync for form submission.

Div

const divRef = useRef(null);

<div ref={divRef}>
  <h2>Existing content</h2>
  <p>This will become editable.</p>
</div>
<RemyxEditor attachTo={divRef} theme="dark" height={400} />

useRemyxEditor Hook

For lower-level control:

import { useRemyxEditor } from '@remyxjs/react';

function MyEditor() {
  const targetRef = useRef(null);
  const { engine, containerRef, editableRef, ready } = useRemyxEditor(
    targetRef,
    {
      onChange: (content) => console.log(content),
      placeholder: 'Type here...',
      theme: 'light',
      height: 400,
    },
  );

  return <textarea ref={targetRef} />;
}

Returns engine (EditorEngine), containerRef, editableRef, and ready (boolean).

useEditorEngine Hook

The lowest-level hook — manages just the engine lifecycle:

import { useEditorEngine } from '@remyxjs/react';

function CustomEditor() {
  const editorRef = useRef(null);
  const engine = useEditorEngine(editorRef, {
    outputFormat: 'html',
    plugins: [],
  });

  return <div ref={editorRef} contentEditable />;
}

Plugins

Plugins

14 optional plugins are available as drag-and-drop folders in remyxjs/plugins/:

  • WordCountPlugin — word/character counts in the status bar

  • PlaceholderPlugin — placeholder text when empty

  • AutolinkPlugin — auto-converts typed URLs to links

  • SyntaxHighlightPlugin — automatic syntax highlighting for code blocks with language detection, theme-aware colors, setCodeLanguage/getCodeLanguage/toggleLineNumbers commands, copy-to-clipboard button, inline <code> mini-highlighting, and extensible language registry (registerLanguage/unregisterLanguage). When active, a language selector dropdown appears on focused code blocks.

  • TablePlugin — enhanced table features including column/row resize handles, click-to-sort on header cells (single + multi-column with Shift), per-column filter dropdowns, inline cell formulas (SUM, AVERAGE, COUNT, MIN, MAX, IF, CONCAT), cell formatting (number, currency, percentage, date), and sticky header rows. Adds 6 new commands: toggleHeaderRow, sortTable, filterTable, clearTableFilters, formatCell, evaluateFormulas. Context menu adds Toggle Header Row, Format Cell options, and Clear Filters when right-clicking in a table.

  • CalloutPlugin — styled callout blocks with 7 built-in types (info, warning, error, success, tip, note, question), custom type registration, collapsible toggle, nested content, GFM alert syntax auto-conversion. Adds 4 commands: insertCallout, removeCallout, changeCalloutType, toggleCalloutCollapse. Context menu adds "Insert Callout" and "Remove Callout".

  • CommentsPlugin — inline comment threads on selected text, resolved/unresolved state with visual indicators, @mention parsing, reply threads, comment-only mode (read-only editor with annotation support), import/export, DOM sync via MutationObserver. Adds 6 new commands: addComment, deleteComment, resolveComment, replyToComment, editComment, navigateToComment. Context menu adds "Add Comment" when text is selected.

  • LinkPlugin — link preview tooltips (via onUnfurl callback), broken link detection with periodic scanning and rmx-link-broken indicators, auto-linking of typed URLs/emails/phone numbers on Space/Enter, link click analytics (onLinkClick), bookmark anchors (insertBookmark, linkToBookmark, getBookmarks, removeBookmark), scanBrokenLinks/getBrokenLinks commands.

  • TemplatePlugin{{merge_tag}} syntax with visual chips, {{#if condition}}...{{/if}} conditional blocks, {{#each items}}...{{/each}} repeatable sections, live preview with sample data, 5 pre-built templates (email, invoice, letter, report, newsletter), registerTemplate/unregisterTemplate/getTemplateLibrary/getTemplate APIs, exportTemplate JSON export. Adds 6 commands: insertMergeTag, loadTemplate, previewTemplate, exitPreview, exportTemplate, getTemplateTags.

  • KeyboardPlugin — Vim mode (normal/insert/visual with h/j/k/l/w/b motions), Emacs keybindings (Ctrl+A/E/F/B/K/Y), custom keybinding map, smart bracket/quote auto-pairing with wrap-selection, multi-cursor (Cmd+D), jump-to-heading (Cmd+Shift+G). Adds 5 commands: setKeyboardMode, getVimMode, jumpToHeading, getHeadings, selectNextOccurrence.

  • DragDropPlugin — drop zone overlays with visual guides, cross-editor drag via text/x-remyx-block transfer, external file/image/rich-text drops, block reorder with ghost preview and drop indicator, onDrop/onFileDrop callbacks. Adds 2 commands: moveBlockUp, moveBlockDown (Cmd+Shift+Arrow).

  • MathPlugin — LaTeX/KaTeX math rendering with $...$ inline and $$...$$ block syntax, pluggable renderMath(latex, displayMode) callback, 40+ symbol palette across 4 categories (Greek, Operators, Arrows, Common), auto equation numbering, latexToMathML() conversion, parseMathExpressions() detection. Adds 6 commands: insertMath, editMath, insertSymbol, getSymbolPalette, getMathElements, copyMathAs.

  • TocPlugin — auto-generated table of contents from H1-H6 heading hierarchy, section numbering (1, 1.1, 1.2), insertToc renders linked <nav> into document, scrollToHeading with smooth scroll, validateHeadings detects skipped levels, onOutlineChange callback + toc:change event. Adds 4 commands: getOutline, insertToc, scrollToHeading, validateHeadings.

  • AnalyticsPlugin — real-time readability scores (Flesch-Kincaid, Gunning Fog, Coleman-Liau, Flesch Reading Ease), reading time estimate, vocabulary level (basic/intermediate/advanced), sentence/paragraph length warnings, goal-based word count with progress tracking, keyword density, SEO hints. onAnalytics callback + analytics:update event. Adds 3 commands: getAnalytics, getSeoAnalysis, getKeywordDensity.

  • SpellcheckPlugin — built-in grammar engine with passive voice detection, wordiness patterns, cliche detection, and punctuation checks. Inline red wavy (spelling), blue wavy (grammar), and green dotted (style) underlines. Right-click context menu with correction suggestions, "Ignore", and "Add to Dictionary". Writing-style presets (formal, casual, technical, academic). BCP 47 multi-language support. Optional customService interface for LanguageTool/Grammarly. Persistent dictionary via localStorage. onError/onCorrection callbacks + spellcheck:update/grammar:check events. Adds 6 commands: toggleSpellcheck, checkGrammar, addToDictionary, ignoreWord, setWritingStyle, getSpellcheckStats.

import {
  SyntaxHighlightPlugin,
  TablePlugin,
  CommentsPlugin,
  CalloutPlugin,
  LinkPlugin,
  TemplatePlugin,
  KeyboardPlugin,
  DragDropPlugin,
  MathPlugin,
  TocPlugin,
  AnalyticsPlugin,
  SpellcheckPlugin,
} from '@remyxjs/react';

<RemyxEditor
  plugins={[
    SyntaxHighlightPlugin(),
    TablePlugin(),
    CommentsPlugin(),
    CalloutPlugin(),
    LinkPlugin(),
    TemplatePlugin(),
    KeyboardPlugin(),
    DragDropPlugin(),
    MathPlugin(),
    TocPlugin(),
    AnalyticsPlugin(),
    SpellcheckPlugin(),
  ]}
/>;

TablePlugin in depth

When TablePlugin() is active, every <table class="rmx-table"> in the editor automatically gains:

| Feature | How it works | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Sortable columns | Click any <th> to cycle through ascending → descending → unsorted. Rows are physically reordered in the DOM so the sort persists in HTML output. | | Multi-column sort | Hold Shift and click additional headers. A small priority number appears on each sorted header. | | Sort data types | The sort auto-detects numeric, date, or alphabetical data. Pass dataType explicitly or provide a custom comparator via the tableSortComparator engine option. | | Sort indicators | ▲/▼ triangles rendered via CSS ::after on <th> elements with data-sort-dir attributes. | | Filterable rows | A small ▽ icon appears in each header cell. Click it to open a filter dropdown with a text input. Rows not matching the filter are hidden (non-destructive — they reappear when the filter is cleared). Multiple columns can be filtered simultaneously (AND logic). | | Column/row resize | Invisible 6px drag handles appear at column and row borders. Drag to resize. The resize is smooth (rAF-driven) and creates an undo snapshot on mouseup. | | Inline formulas | Type =SUM(A1:A5) in any cell. On blur, the formula is stored in a data-formula attribute and the computed result is displayed. On focus, the formula text is shown for editing. Supports SUM, AVERAGE, COUNT, MIN, MAX, IF, CONCAT, cell references (A1 notation), ranges, arithmetic, and comparison operators. Circular references are detected and display #CIRC!. | | Cell formatting | Right-click a cell to format as Number, Currency, Percentage, or Date. The raw value is preserved in data-raw-value; the display uses Intl.NumberFormat / Intl.DateTimeFormat. | | Sticky header | <thead><th> cells use position: sticky so the header row stays visible when scrolling tall tables. | | Clipboard interop | Copying table content produces both HTML and TSV (tab-separated values). Pasting TSV or HTML tables from Excel / Google Sheets inserts data into the grid starting at the caret cell, auto-expanding rows and columns as needed. |

Custom sort comparator:

<RemyxEditor
  plugins={[TablePlugin()]}
  engineOptions={{
    tableSortComparator: (a, b, dataType, columnIndex) => {
      // Custom comparison logic — return negative, zero, or positive
      return a.localeCompare(b);
    },
  }}
/>

Programmatic commands (via engine ref):

import { useRef } from 'react';

function MyEditor() {
  const engineRef = useRef(null);

  const handleSort = () => {
    engineRef.current?.executeCommand('sortTable', {
      keys: [
        { columnIndex: 0, direction: 'asc' },
        { columnIndex: 2, direction: 'desc', dataType: 'numeric' },
      ],
    });
  };

  const handleFilter = () => {
    engineRef.current?.executeCommand('filterTable', {
      columnIndex: 1,
      filterValue: 'active',
    });
  };

  return (
    <>
      <button onClick={handleSort}>Sort by Name then Score</button>
      <button onClick={handleFilter}>Show active only</button>
      <RemyxEditor
        plugins={[TablePlugin()]}
        onReady={(engine) => {
          engineRef.current = engine;
        }}
      />
    </>
  );
}

Context menu additions (appear when right-clicking inside a table):

  • Toggle Header Row
  • Format as Number / Currency / Percentage / Date
  • Clear Filters

CalloutPlugin

The CalloutPlugin adds styled callout/alert/admonition blocks with collapsible toggle and GFM alert auto-conversion:

import { CalloutPlugin } from '@remyxjs/react';

<RemyxEditor plugins={[CalloutPlugin()]} />;

Insert callouts programmatically:

// Basic callout
engine.executeCommand('insertCallout', { type: 'warning' });

// Collapsible with custom title
engine.executeCommand('insertCallout', {
  type: 'tip',
  collapsible: true,
  title: 'Pro tip',
});

// With pre-populated content
engine.executeCommand('insertCallout', {
  type: 'info',
  content: '<ul><li>Step 1</li><li>Step 2</li></ul>',
});

// Change type of the focused callout
engine.executeCommand('changeCalloutType', 'error');

// Register a custom callout type
import { registerCalloutType } from '@remyxjs/react';
registerCalloutType({
  type: 'security',
  label: 'Security',
  icon: '🔒',
  color: '#dc2626',
});

GitHub-flavored alerts — blockquotes with > [!NOTE], > [!WARNING], etc. are automatically converted to callout blocks on paste or typing.

Available types: info, warning, error, success, tip, note, question (plus any custom types).

CommentsPlugin

The CommentsPlugin adds inline comment threads — highlight text, attach discussion threads, resolve/reopen, reply, and @mention other users.

import { CommentsPlugin } from '@remyxjs/react';

<RemyxEditor
  plugins={[
    CommentsPlugin({
      onComment: (thread) => saveToServer(thread),
      onResolve: ({ thread, resolved }) => updateServer(thread),
      onDelete: (thread) => deleteFromServer(thread),
      onReply: ({ thread, reply }) => notifyUser(reply),
      mentionUsers: ['alice', 'bob', 'charlie'],
    }),
  ]}
/>;

Comment-only mode — make the editor read-only but still allow annotations:

<RemyxEditor plugins={[CommentsPlugin({ commentOnly: true })]} />

useComments hook

The useComments hook provides reactive state and convenience methods:

import { useComments, CommentsPanel } from '@remyxjs/react';

function AnnotatedEditor() {
  const engineRef = useRef(null);
  const {
    threads, activeThread, addComment, deleteComment,
    resolveComment, replyToComment, navigateToComment,
    exportThreads, importThreads,
  } = useComments(engineRef.current);

  return (
    <div style={{ display: 'flex' }}>
      <RemyxEditor
        plugins={[CommentsPlugin({ mentionUsers: ['alice', 'bob'] })]}
        onReady={(engine) => { engineRef.current = engine; }}
        style={{ flex: 1 }}
      />
      <CommentsPanel
        threads={threads}
        activeThread={activeThread}
        onNavigate={navigateToComment}
        onResolve={(id, resolved) => resolveComment(id, resolved)}
        onDelete={deleteComment}
        onReply={(id, params) => replyToComment(id, params)}
        filter="all"  {/* 'all' | 'open' | 'resolved' */}
      />
    </div>
  );
}

CommentsPanel props

| Prop | Type | Description | | -------------- | -------------------------------------- | ------------------------------------- | | threads | CommentThread[] | All threads (from useComments) | | activeThread | CommentThread \| null | Currently focused thread | | onNavigate | (threadId) => void | Called when a thread card is clicked | | onResolve | (threadId, resolved) => void | Called when Resolve/Reopen is clicked | | onDelete | (threadId) => void | Called when Delete is clicked | | onReply | (threadId, { author, body }) => void | Called when a reply is submitted | | filter | 'all' \| 'open' \| 'resolved' | Which threads to display | | className | string | Additional CSS class |

Engine events

| Event | Payload | When | | ------------------- | ---------------------- | --------------------------- | | comment:created | { thread } | New comment added | | comment:resolved | { thread, resolved } | Thread resolved or reopened | | comment:deleted | { thread } | Thread deleted | | comment:replied | { thread, reply } | Reply added to thread | | comment:updated | { thread } | Thread body edited | | comment:clicked | { thread, element } | Comment highlight clicked | | comment:navigated | { threadId } | Scrolled to comment | | comment:imported | { count } | Threads imported |

Custom Plugins

import { createPlugin } from '@remyxjs/react';

const MyPlugin = createPlugin({
  name: 'my-plugin',
  version: '1.0.0',
  description: 'Example plugin with lifecycle hooks',
  author: 'You',

  // Lifecycle hooks (auto-wired, sandboxed)
  onContentChange(api) {
    console.log('Content changed:', api.getText().length, 'chars');
  },
  onSelectionChange(api) {
    console.log('Selection:', api.getActiveFormats());
  },

  // Traditional init/destroy
  init(api) {
    /* called once on mount */
  },
  destroy(api) {
    /* called on unmount */
  },

  // Dependencies — ensures 'other-plugin' is initialized first
  dependencies: ['other-plugin'],

  // Scoped settings with validation
  settingsSchema: [
    {
      key: 'maxLength',
      type: 'number',
      label: 'Max Length',
      defaultValue: 5000,
      validate: (v) => v > 0,
    },
    {
      key: 'mode',
      type: 'select',
      label: 'Mode',
      defaultValue: 'auto',
      options: [
        { label: 'Auto', value: 'auto' },
        { label: 'Manual', value: 'manual' },
      ],
    },
  ],
  defaultSettings: { maxLength: 5000, mode: 'auto' },
});
<RemyxEditor plugins={[MyPlugin]} />

Access plugin settings from outside:

<RemyxEditor
  plugins={[MyPlugin]}
  onReady={(engine) => {
    engine.plugins.getPluginSetting('my-plugin', 'maxLength'); // 5000
    engine.plugins.setPluginSetting('my-plugin', 'maxLength', 3000); // validates + updates
  }}
/>

Plugin with Custom Commands

const HighlightPlugin = createPlugin({
  name: 'highlight',
  init(engine) {
    engine.executeCommand('highlight');
  },
  commands: [
    {
      name: 'highlight',
      execute(eng) {
        document.execCommand('backColor', false, '#ffeb3b');
      },
    },
  ],
});

Read-Only Mode

Disable editing while keeping the full rendered output visible.

<RemyxEditor value={content} readOnly={true} />

Combine with a toggle for preview mode:

const [editing, setEditing] = useState(true);

<button onClick={() => setEditing(!editing)}>
  {editing ? 'Preview' : 'Edit'}
</button>
<RemyxEditor value={content} onChange={setContent} readOnly={!editing} />

Error Handling

onError Callback

Catch errors from plugins, the engine, and file uploads without crashing the app:

<RemyxEditor
  onError={(error, info) => {
    if (info.source === 'plugin') {
      console.warn(`Plugin "${info.pluginName}" failed:`, error);
    } else if (info.source === 'upload') {
      alert(`Upload failed: ${error.message}`);
    } else if (info.source === 'engine') {
      console.error(`Engine error in ${info.phase}:`, error);
    }
  }}
/>

Error sources:

| Source | Info Fields | Description | | ---------- | ------------ | --------------------------- | | 'plugin' | pluginName | Plugin init/destroy failure | | 'engine' | phase | Engine initialization error | | 'upload' | file | Upload handler rejection |

Custom Error Fallback

Replace the default error UI with your own component:

<RemyxEditor
  errorFallback={
    <div className="editor-error">
      <p>Something went wrong. Please refresh.</p>
    </div>
  }
/>

Engine Access

Use the onReady callback to access the EditorEngine instance for programmatic control.

Execute Commands

functi