EIRACORE / REACT PACKAGE

react-eira-editorFlexible editing. Simple integration.

A React and TypeScript rich text editor built on Lexical. Work with HTML, choose the plugins you need and connect to your existing forms.

Works well with Ant Design Form (Ant Form), React Hook Form and Formik through value, onChange, onBlur and ref. See form integrations →

React 18.2+ / 19TypeScriptHTML valueVI / EN

00 Demo

Try the standalone editor with all tools: formatting, fonts, alignment, lists, links, images, attachments, tables, undo/redo, HTML, preview and @ mentions.

Đang tải editor / Loading editor…

Installation

Run one of these commands in your React project. Peer dependencies: react and react-dom ^18.2.0 or ^19.0.0.

Terminal
npm install @eira-core/react-eira-editor

# pnpm
pnpm add @eira-core/react-eira-editor

# Yarn
yarn add @eira-core/react-eira-editor

Import style.css once for the editor UI. In Next.js App Router, use Editor inside a Client Component ('use client').

Start with HTML

value accepts HTML and onChange returns updated HTML. This controlled example enables headings, text formatting and undo/redo.

ArticleEditor.tsx
'use client';

import { useState } from 'react';
import { Editor } from '@eira-core/react-eira-editor';
import { formattingPlugin } from '@eira-core/react-eira-editor/plugins/formatting';
import { headingPlugin } from '@eira-core/react-eira-editor/plugins/headings';
import { historyPlugin } from '@eira-core/react-eira-editor/plugins/history';
import '@eira-core/react-eira-editor/style.css';

// Keep the plugin configuration outside render.
const plugins = [headingPlugin, formattingPlugin, historyPlugin];

export default function ArticleEditor() {
  const [html, setHTML] = useState('<p>Hello, Eira!</p>');

  return (
    <Editor
      plugins={plugins}
      value={html}
      onChange={setHTML}
      locale="vi"
      minHeight={240}
      aria-label="Article content"
    />
  );
}

Uncontrolled usage

Use defaultValue when you do not need to keep content in state. The following snippets reuse plugins and state from the example above unless they define their own configuration.

JSX
<Editor
  plugins={plugins}
  defaultValue="<p>Start writing here.</p>"
  onChange={(html) => console.log(html)}
/>

Choose the features you need

Without plugins, Editor provides basic HTML editing with no toolbar. Import plugins from their individual entry points and add them to plugins in the desired toolbar order.

Plugin import
import { tablePlugin } from '@eira-core/react-eira-editor/plugins/table';

const plugins = [tablePlugin];
/plugins/…ExportFeatures
formattingformattingPluginBold, italic, underline, strikethrough
headingsheadingPluginParagraphs, H1–H6 and quote schema
font-sizefontSizePluginFont size and inherited reset
font-familyfontFamilyPlugin / createFontFamilyPluginFont family and custom font choices
line-heightlineHeightPluginBlock line spacing
indentindentPluginBlock indentation and list nesting
text-aligntextAlignPluginLeft, center, right and justified alignment
scriptsscriptPluginSubscript and superscript
colorscolorPluginText and background colors
listslistPluginBullet and numbered lists
linklinkPluginInsert, edit and remove links
imagecreateImagePluginImage upload, width and alignment
filecreateFilePluginAttachment upload and download
tabletablePluginTables, row/column actions and column resizing
historyhistoryPluginUndo and redo
htmlhtmlPluginRead-only view of current HTML
clear-formatclearFormatPluginClear selection formatting
previewpreviewPluginPreview dialog in a sandboxed iframe
inline-toolbarinlineToolbarPlugin / createInlineToolbarPluginFloating toolbar for selected text
mentionmentionPlugin@ user suggestions

Define plugins outside render or memoize with useMemo. Configuration defines the document schema; remount with a new key and compatible HTML to change it. Plugin names must be unique.

Props / API

All props are optional. Import EditorProps, EditorStyles, EditorClassNames and other types from the main entry point.

PropTypeDefaultDescription
valuestring—HTML for controlled mode; external replacements reset history.
defaultValuestring''Initial HTML for uncontrolled mode, read only on mount.
onChange(html: string) => void—Receives changed HTML without debounce. Empty content exports as an empty string.
pluginsreadonly EditorPlugin[][]Registers features and schema; array order controls toolbar order. Keep stable for the editor lifetime.
placeholderstringlocaleEmpty content hint; defaults to “Nhập nội dung...” in vi and “Enter content...” in en.
locale'vi' | 'en''vi'Editor UI language; can change while editing.
messagesEditorMessages—Overrides translations using Vietnamese source keys. Missing keys use the current locale.
disabledbooleanfalseDisables editing and actions; removes the editable area from the tab order.
readOnlybooleanfalseRead-only content with the editor interface still visible.
previewbooleanfalseContent only: hides toolbars, border, padding and placeholder. Links and downloads remain available.
toolbarbooleantrueShows or hides the main toolbar; plugins and contextual tools remain active.
mentionbooleanfalseEnables @ suggestions; requires mentionPlugin.
mentionOptionsMentionOptions{}Local items or remote user search, debounce, limits and callbacks.
minHeightCSSProperties['minHeight']180pxMinimum content height; defaults to zero in preview.
maxHeightCSSProperties['maxHeight']—Maximum content height; accepts a number or CSS string.
autoFocusboolean | 'start' | 'end'falseFocuses once when first editable; true is equivalent to end.
classNamestring''Additional CSS class on the wrapper.
classNamesEditorClassNames—Classes for wrapper, toolbar and content; built-in classes are retained.
stylesEditorStyles—Inline styles for wrapper, toolbar and content. styles.content overrides minHeight/maxHeight.
idstring—ID of the contenteditable element.
namestring—Stored as data-name metadata; does not submit content through native FormData.
onFocus / onBlur() => void—Fires when focus enters or leaves the editor, including its tools and dialogs.
onReady(api: EditorRef) => void—Called once after initialization with the same API exposed by ref.
onPasteClipboardEventHandler<HTMLDivElement>—Runs before default paste; call preventDefault() to handle insertion yourself.
onError(error: Error) => voidthrowHandles editor errors; errors are thrown when omitted.
aria-label / aria-labelledby / aria-describedby / aria-invalid / aria-requiredReact AriaAttributes—Forwarded to the editable area; provide an explicit accessible label in forms.

Edit, read only or display content

readOnly prevents edits while keeping the interface. preview displays content in place without editor tools or empty spacing. Reuse the document’s plugins so images, tables and attachments render correctly.

JSX
<Editor value={html} plugins={plugins} preview />

// Hide the main toolbar while keeping plugin behavior.
<Editor value={html} onChange={setHTML} plugins={plugins} toolbar={false} />

Toggle preview without remounting; switching modes does not emit onChange or discard history. previewPlugin is a separate feature that opens a preview dialog.

Upload images and files to your API

Supply uploadImage or uploadFile callbacks that return a URL. The editor inserts content only after a successful upload; signal allows cancellation when closing the dialog or unmounting.

uploads.tsx
import { createImagePlugin } from '@eira-core/react-eira-editor/plugins/image';
import { createFilePlugin } from '@eira-core/react-eira-editor/plugins/file';

// This endpoint belongs to your application.
async function uploadFile(file: File, { signal }: { signal: AbortSignal }) {
  const body = new FormData();
  body.append('file', file);
  const response = await fetch('/api/uploads', { method: 'POST', body, signal });
  if (!response.ok) throw new Error('Upload failed');
  const { url } = await response.json();
  return url; // An HTTP(S) URL or a relative path.
}

const plugins = [
  createImagePlugin({ uploadImage: uploadFile, maxFileSize: 10 * 1024 * 1024 }),
  createFilePlugin({ uploadFile, accept: '.pdf,.docx', maxFileSize: 20 * 1024 * 1024 }),
];

<Editor plugins={plugins} value={html} onChange={setHTML} />

Without callbacks, images/files are embedded as local data URLs with a default 10 MiB limit. Images support PNG, JPEG, WebP, GIF and AVIF. accept is only a file picker hint; validate file types and sizes in your API.

Mention users with @

JSX
import { mentionPlugin } from '@eira-core/react-eira-editor/plugins/mention';

const plugins = [mentionPlugin];
const mentionOptions = {
  items: [{ id: 'u1', label: 'Nguyễn An', description: 'Developer' }],
};

<Editor plugins={plugins} mention mentionOptions={mentionOptions} />

MentionItem requires id and label, with optional avatar and description. Use loadItems(query, { signal }) returning a Promise instead of items for API search. MentionOptions supports minChars (0), debounceMs (250), limit (8), renderItem, onSelect and onClick.

Connect to your forms

The value/onChange/onBlur API and ref work with popular form libraries. These are separate integration snippets; import Controller from react-hook-form, Form from antd or useField from formik in the corresponding application.

Form integrations
// React Hook Form — inside a component using useForm().
<Controller
  name="content"
  control={control}
  render={({ field }) => (
    <Editor {...field} plugins={plugins} aria-label="Content" />
  )}
/>

// Ant Design
<Form.Item name="content" label="Content">
  <Editor plugins={plugins} aria-label="Content" />
</Form.Item>

// Formik — inside a component using useField('content').
<Editor
  value={field.value}
  onChange={(html) => helpers.setValue(html)}
  onBlur={() => helpers.setTouched(true)}
  plugins={plugins}
  aria-label="Content"
/>

name is metadata, not a hidden input. For native HTML submission, add the value to FormData yourself. Give the editable area an aria-label or aria-labelledby.

Control the editor with ref

EditorWithActions.tsx
import { useRef } from 'react';
import { Editor, type EditorRef } from '@eira-core/react-eira-editor';

export function EditorWithActions() {
  const editorRef = useRef<EditorRef>(null);

  return (
    <>
      <Editor ref={editorRef} plugins={plugins} defaultValue="<p>Hello!</p>" />
      <button type="button" onClick={() => editorRef.current?.focus('end')}>
        Focus
      </button>
      <button type="button" onClick={() => editorRef.current?.insertHTML('<p>New paragraph</p>')}>
        Insert HTML
      </button>
    </>
  );
}
MethodResult
focus(position?), blur()Focus at start/end or blur the editor.
getHTML(), getText(), isEmpty()Read HTML, plain text or empty state in any mode.
setHTML(html), insertHTML(html), clear()Mutate content, return boolean; emit onChange when HTML changes.
canUndo(), canRedo(), undo(), redo()Return boolean; use historyPlugin for undo/redo history.

Content mutations return false in disabled, readOnly or preview mode. Use onReady(api) to receive the same API without a ref.

Integration notes

  • HTML is converted through the plugin schema, not preserved byte for byte. Missing plugins may cause unsupported nodes to be dropped.
  • Echoing onChange back into value preserves the caret. External value replacements reset history; ref mutations are undoable with historyPlugin.
  • Validate and sanitize HTML at your application’s storage/render boundary. Preserve data-eira-mention, data-mention-id and data-eira-file metadata for mentions/attachments.
  • Nested tables and merged-cell editing are unsupported. Images support width and alignment controls; drag-and-drop upload and pointer image resizing are not implemented.
View package and README on npm ↗

← EiraCore