RJ Editor
rj-editor is a React text editor package for building article editors, admin panels, CMS forms, learning platforms, notes, documentation tools, and other content-heavy interfaces.
It provides a ready-to-use RJTextEditor component with a tabbed toolbar, text formatting, tables, links, images, YouTube embeds, fullscreen mode, i18n, theming, and Ant Design Form support.
Features
- Text formatting: bold, italic, underline, strikethrough, subscript, superscript.
- Style controls: font size, font family, text color, background color, clear formatting.
- Paragraph tools: alignment, ordered list, unordered list, indent, outdent, line and paragraph spacing.
- Insert tools: table, link, image, YouTube video, and editable code blocks.
- Code blocks: editable code blocks, custom headers, language selection, syntax highlighting, lightweight completions, and persistent copy controls in both the editor and rendered HTML.
- Table editing: add/remove rows and columns, merge/split cells, header row/column, cell background, vertical alignment, delete table.
- Image editing: upload, drag-and-drop, paste from clipboard, resize, align, alt text, caption, link, border, border radius, shadow, object fit, wrapping, rotate, replace, delete.
- Browser fullscreen mode.
- Optional footer status bar with words, characters, selection, language, mode, and zoom indicators.
- Ant Design
Form.Itemintegration. - Built-in
uz,en, andrutranslations. - Custom locale and partial translation override support.
- Theme customization through CSS custom properties.
Installation
npm install rj-editorreact and react-dom are peer dependencies. They must already exist in your application:
npm install react react-domQuick Start
import { RJTextEditor } from 'rj-editor'
export function App() {
return (
<RJTextEditor
autofocus
locale="en"
placeholder="Write your content..."
onChange={(html) => {
console.log(html)
}}
/>
)
}You do not need to import a separate stylesheet. The package styles are included when RJTextEditor is imported.
Saving Content
onChange returns the editor content as an HTML string in the first argument. This is the value you usually store in your database.
<RJTextEditor
onChange={(html) => {
saveDraft(html)
}}
/>The callback also exposes a JSON string and the current editor state for advanced use cases:
type OnChange = (
html: string,
json: string,
editorState: unknown,
) => voidRendering Saved HTML
Render the saved HTML string anywhere in your application:
export function Article({ html }: { html: string }) {
return <article dangerouslySetInnerHTML={{ __html: html }} />
}Exported content includes inline styles for editor-only elements such as code blocks, inline code, tables, images, YouTube embeds, and equations. This lets saved HTML render correctly in pages that do not mount the editor stylesheet.
Exported code blocks keep their header and copy button. The exported copy button includes a small inline fallback, but many applications sanitize HTML or block inline handlers with CSP. For reliable code-block copy controls on pages that only render saved HTML, import the lightweight rendered HTML helper in that client application:
import 'rj-editor/rendered-html'
export function Article({ html }: { html: string }) {
return <article dangerouslySetInnerHTML={{ __html: html }} />
}This helper does not mount the editor. It only registers the delegated copy handler for exported code blocks. If you sanitize saved HTML before rendering it, keep the exported style, data-rj-editor-code-block, data-rj-editor-code-copy, and data-rj-editor-code-content attributes if you want the built-in rendered styles and copy controls to remain active. The onclick attribute is optional when the helper is imported.
Only render trusted or sanitized HTML. Sanitize untrusted user-provided HTML before passing it to dangerouslySetInnerHTML.
Initial Content
Use defaultValue when you want to load saved HTML once when the editor mounts:
<RJTextEditor defaultValue="<p>Hello world</p>" />Use value when your application controls the editor content from React state:
import { useState } from 'react'
import { RJTextEditor } from 'rj-editor'
export function ControlledEditor() {
const [content, setContent] = useState('<p>Initial content</p>')
return (
<RJTextEditor
value={content}
onChange={(html) => setContent(html)}
/>
)
}Ant Design Form
RJTextEditor works inside Ant Design Form.Item. The form receives the HTML string as the field value.
import { Button, Form } from 'antd'
import { RJTextEditor } from 'rj-editor'
export function ArticleForm() {
return (
<Form
layout="vertical"
onFinish={(values) => {
console.log(values.content)
}}
>
<Form.Item
label="Content"
name="content"
rules={[{ required: true, message: 'Content is required' }]}
>
<RJTextEditor placeholder="Write your article..." />
</Form.Item>
<Button htmlType="submit" type="primary">
Save
</Button>
</Form>
)
}When validation fails, the editor border follows the Ant Design error state.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
autofocus |
boolean |
false |
Focuses the editor after mount. |
className |
string |
undefined |
Adds a custom class to the editor root. |
defaultValue |
string |
undefined |
Initial HTML content. Applied once on mount. |
footer |
(stats) => React.ReactNode |
undefined |
Custom footer renderer. Works when showFooter is enabled. |
footerItems |
RJEditorFooterItem[] |
built-in status items | Controls which default footer items are shown. |
footerLanguageLabel |
string |
derived from locale |
Language label shown in the footer. |
footerZoom |
number |
100 |
Zoom value shown in the footer. Display-only for now. |
id |
string |
undefined |
Adds an id to the editor root. |
locale |
'uz' | 'en' | 'ru' | string |
'en' |
Active editor language. |
locales |
Record<string, DeepPartial<RJEditorTranslations>> |
undefined |
Adds custom locales. |
namespace |
string |
'RJEditor' |
Unique editor namespace. Useful when multiple editor instances exist on one page. |
onBlur |
React.FocusEventHandler<HTMLDivElement> |
undefined |
Called when the editor root loses focus. |
onChange |
(html, json, editorState) => void |
undefined |
Called whenever editor content changes. |
onFocus |
React.FocusEventHandler<HTMLDivElement> |
undefined |
Called when the editor root receives focus. |
onUpload |
(file: File) => Promise<string> |
base64 fallback | Uploads image files and returns the image URL to store in editor content. |
placeholder |
string |
Locale-based text | Placeholder shown when the editor is empty. |
showFooter |
boolean |
false |
Shows the optional editor footer status bar. |
translations |
DeepPartial<RJEditorTranslations> |
undefined |
Overrides translations for the active locale. |
theme |
'light' | 'dark' | 'auto' |
'auto' |
Controls the editor color theme. Auto follows the document color scheme and system preference. |
value |
string |
undefined |
Controlled HTML content. |
Toolbar
The toolbar is organized by tabs:
Home: text formatting, styles, lists, alignment, indentation, and spacing.Insert: table, link, image, YouTube video, and code block insertion.Image: shown when an image is selected.Table: shown when the cursor is inside a table.
Contextual tabs are only visible when they are useful, keeping the editor interface focused.
Footer Status Bar
Enable the optional footer when you want Word-like status information under the editor:
<RJTextEditor showFooter />The default footer shows word count, character count, selected text count, language, editor mode, and zoom. You can choose a smaller set:
<RJTextEditor
showFooter
footerItems={['words', 'characters', 'selection']}
/>For a fully custom footer, pass a render function:
<RJTextEditor
showFooter
footer={(stats) => (
<div>
{stats.words} words · {stats.characters} characters
</div>
)}
/>Images
Images can be inserted from the local file picker, drag-and-drop, or clipboard paste. By default, the editor converts images to base64 data URLs and stores them inside the HTML content. For production, pass onUpload so images are uploaded to your own server or storage service and only the returned URL is stored in the content.
Important details:
- Maximum image size is
5MB. - If
onUploadis not provided, base64 fallback keeps local/demo usage working. onUploadis used for image insert, paste, drop, and replace actions.- While the upload promise is pending, image controls show loading and are disabled, including uploads started by clipboard paste and drag-and-drop.
- Duplicate clipboard file entries are de-duplicated before insertion and active uploads for the same file share one promise.
- If the upload promise rejects, the image is not inserted/replaced and the editor shows the localized upload error.
Upload example:
<RJTextEditor
onUpload={async (file) => {
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/uploads/images', {
method: 'POST',
body: formData,
})
if (!response.ok) {
throw new Error('Image upload failed')
}
const data = await response.json()
return data.url
}}
/>The returned string must be the final image URL used in exported HTML:
<img src="https://cdn.example.com/uploads/image.webp" alt="..." />Image tools include:
- left, center, and right alignment;
- custom width percentage;
- quick resize: 25%, 50%, 100%;
- alt text;
- caption;
- link;
- border;
- border radius;
- shadow;
- object fit: contain, cover, fill;
- text wrapping: top-bottom and square;
- rotate left/right;
- replace image;
- delete image.
Tables
Tables can be created from the Insert tab with a grid picker or by entering row and column counts.
Available table tools:
- add row above/below;
- add column left/right;
- delete row/column;
- merge cells;
- split cells;
- toggle header row;
- toggle header column;
- set cell background;
- set vertical alignment;
- delete table.
Exported tables include inline border, header background, selected-cell cleanup, and rounded corner styles. The exported table palette uses neutral translucent colors so saved tables remain readable when the consuming page switches between light and dark mode.
Links
Links are created with a custom modal. The modal supports URL and display text fields and works correctly in fullscreen mode.
YouTube Embeds
Paste a YouTube URL from the Insert tab to add an embedded video. The editor converts supported YouTube links into an iframe embed.
Code Blocks
Insert a code block from the Insert tab. Code blocks support:
- direct editing inside the document;
- a custom header/title, for example
package.json,Terminal, ornpm; - searchable language selection with presets such as Plain Text, Terminal, JSON, Bash, Shell, PNPM, NPM, Yarn, Bun, JavaScript, JSX, TypeScript, TSX, HTML, CSS, YAML, Dockerfile, SQL, Python, Rust, and more;
- syntax highlighting when a Prism grammar is available;
- Bash-style highlighting aliases for Terminal, Shell, PNPM, NPM, Yarn, and Bun;
- lightweight code completions for supported languages and command snippets;
- a copy button that remains visible when the block is not focused;
- a persistent copy button in exported HTML rendered with
dangerouslySetInnerHTML; - normalized paste handling for common documentation code panels, including tabbed package-manager examples;
- HTML round trips through
valueanddefaultValuewithout copying toolbar controls into the editable content.
The code block header title is independent from the selected language. Use the title for the label users should see, and use the language selector for syntax highlighting and completions.
Completions appear while typing inside a code block. Use ArrowUp and ArrowDown to move through suggestions, Enter or Tab to apply one, and Escape to close the menu.
Fullscreen
The fullscreen button uses the browser Fullscreen API. The editor is opened in real fullscreen mode, not only enlarged with CSS.
Internationalization
Built-in locales:
<RJTextEditor locale="uz" />
<RJTextEditor locale="en" />
<RJTextEditor locale="ru" />Override only the text you need:
<RJTextEditor
locale="en"
translations={{
placeholders: {
editor: 'Start writing...',
},
}}
/>Add a custom locale:
<RJTextEditor
locale="kaa"
locales={{
kaa: {
placeholders: {
editor: 'Maqalanı usı jerge jazıń...',
},
tabs: {
home: 'Bas bet',
insert: 'Qosıw',
},
},
}}
/>Theme
Use the theme prop to force a theme or allow the editor to resolve it automatically:
<RJTextEditor theme="dark" />
<RJTextEditor theme="light" />
<RJTextEditor theme="auto" />light and dark always override the document and system theme. auto is
the default. It follows the HTML document's active color-scheme and then the
user's system preference:
html {
color-scheme: light dark;
}Applications can also change the active document scheme at runtime:
document.documentElement.style.colorScheme = 'dark'
// Also supported: data-theme, data-color-scheme, or data-rj-editor-theme.The same theme attributes and dark / light classes can be placed on
html or body.
Control the theme from an application action:
import { useState } from 'react'
import { RJTextEditor, type RJEditorTheme } from 'rj-editor'
export function ThemeableEditor() {
const [theme, setTheme] = useState<RJEditorTheme>('light')
return (
<>
<button
onClick={() => setTheme((current) => (
current === 'dark' ? 'light' : 'dark'
))}
type="button"
>
Toggle theme
</button>
<RJTextEditor theme={theme} />
</>
)
}The built-in dark theme uses a neutral gray palette. rj-editor also exposes CSS custom properties so you can match your design system.
:root {
--rj-color-brand: #2563eb;
--rj-color-brand-tint: #eaf2ff;
--rj-color-surface: #ffffff;
--rj-color-surface-soft: #f8fafc;
--rj-color-border: #d9dde5;
--rj-color-text: #111827;
--rj-editor-bg: var(--rj-color-surface);
--rj-editor-border: var(--rj-color-border);
--rj-editor-text: var(--rj-color-text);
--rj-editor-toolbar-bg: var(--rj-color-surface-soft);
}Common variables:
| Variable | Description |
|---|---|
--rj-editor-bg |
Editor content background. |
--rj-editor-border |
Editor border color. |
--rj-editor-text |
Main text color. |
--rj-editor-placeholder |
Placeholder color. |
--rj-editor-toolbar-bg |
Toolbar background. |
--rj-editor-toolbar-button-bg |
Toolbar button background. |
--rj-editor-toolbar-button-active-bg |
Active toolbar button background. |
--rj-editor-link |
Link color. |
--rj-editor-focus-border |
Focus border color. |
--rj-editor-error-border |
Validation error border color. |
--rj-editor-warning-border |
Validation warning border color. |
--rj-editor-table-border |
Table border color. |
--rj-editor-table-header-bg |
Table header background. |
--rj-editor-table-selected-bg |
Selected table cell background. |
--rj-editor-inline-code-bg |
Inline code background. |
--rj-editor-inline-code-border |
Inline code border color. |
--rj-editor-inline-code-text |
Inline code text color. |
--rj-editor-code-bg |
Code block background. |
--rj-editor-code-header-bg |
Code block header background. |
--rj-editor-code-border |
Code block border and scrollbar color. |
--rj-editor-code-text |
Code block base text color. |
--rj-editor-code-muted |
Code block header and muted text color. |
--rj-editor-code-keyword |
Code token keyword color. |
--rj-editor-code-string |
Code token string color. |
--rj-editor-code-number |
Code token number color. |
--rj-editor-code-comment |
Code token comment color. |
--rj-editor-code-function |
Code token function/class color. |
--rj-editor-code-operator |
Code token operator/property/punctuation color. |
--rj-editor-code-tag |
Code token tag/deleted color. |
--rj-editor-image-border |
Image selection border color. |
--rj-editor-image-shadow |
Image shadow value. |
--rj-editor-danger |
Dangerous action color. |
Override only the dark theme by targeting the editor theme attribute:
[data-rj-editor-theme='dark'] {
--rj-color-surface: #1d1d1d;
--rj-color-surface-soft: #252525;
--rj-color-border: #444444;
--rj-color-text: #eeeeee;
}SSR
rj-editor depends on browser APIs such as DOM selection, clipboard, file input, and fullscreen. In SSR frameworks, render the editor only on the client.
Example for Next.js:
import dynamic from 'next/dynamic'
const RJTextEditor = dynamic(
() => import('rj-editor').then((mod) => mod.RJTextEditor),
{ ssr: false },
)License
MIT