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.
npm install @eira-core/react-eira-editor
# pnpm
pnpm add @eira-core/react-eira-editor
# Yarn
yarn add @eira-core/react-eira-editorImport 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.
'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.
<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.
import { tablePlugin } from '@eira-core/react-eira-editor/plugins/table';
const plugins = [tablePlugin];| /plugins/… | Export | Features |
|---|---|---|
formatting | formattingPlugin | Bold, italic, underline, strikethrough |
headings | headingPlugin | Paragraphs, H1–H6 and quote schema |
font-size | fontSizePlugin | Font size and inherited reset |
font-family | fontFamilyPlugin / createFontFamilyPlugin | Font family and custom font choices |
line-height | lineHeightPlugin | Block line spacing |
indent | indentPlugin | Block indentation and list nesting |
text-align | textAlignPlugin | Left, center, right and justified alignment |
scripts | scriptPlugin | Subscript and superscript |
colors | colorPlugin | Text and background colors |
lists | listPlugin | Bullet and numbered lists |
link | linkPlugin | Insert, edit and remove links |
image | createImagePlugin | Image upload, width and alignment |
file | createFilePlugin | Attachment upload and download |
table | tablePlugin | Tables, row/column actions and column resizing |
history | historyPlugin | Undo and redo |
html | htmlPlugin | Read-only view of current HTML |
clear-format | clearFormatPlugin | Clear selection formatting |
preview | previewPlugin | Preview dialog in a sandboxed iframe |
inline-toolbar | inlineToolbarPlugin / createInlineToolbarPlugin | Floating toolbar for selected text |
mention | mentionPlugin | @ 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.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | HTML for controlled mode; external replacements reset history. |
defaultValue | string | '' | 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. |
plugins | readonly EditorPlugin[] | [] | Registers features and schema; array order controls toolbar order. Keep stable for the editor lifetime. |
placeholder | string | locale | Empty 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. |
messages | EditorMessages | — | Overrides translations using Vietnamese source keys. Missing keys use the current locale. |
disabled | boolean | false | Disables editing and actions; removes the editable area from the tab order. |
readOnly | boolean | false | Read-only content with the editor interface still visible. |
preview | boolean | false | Content only: hides toolbars, border, padding and placeholder. Links and downloads remain available. |
toolbar | boolean | true | Shows or hides the main toolbar; plugins and contextual tools remain active. |
mention | boolean | false | Enables @ suggestions; requires mentionPlugin. |
mentionOptions | MentionOptions | {} | Local items or remote user search, debounce, limits and callbacks. |
minHeight | CSSProperties['minHeight'] | 180px | Minimum content height; defaults to zero in preview. |
maxHeight | CSSProperties['maxHeight'] | — | Maximum content height; accepts a number or CSS string. |
autoFocus | boolean | 'start' | 'end' | false | Focuses once when first editable; true is equivalent to end. |
className | string | '' | Additional CSS class on the wrapper. |
classNames | EditorClassNames | — | Classes for wrapper, toolbar and content; built-in classes are retained. |
styles | EditorStyles | — | Inline styles for wrapper, toolbar and content. styles.content overrides minHeight/maxHeight. |
id | string | — | ID of the contenteditable element. |
name | string | — | 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. |
onPaste | ClipboardEventHandler<HTMLDivElement> | — | Runs before default paste; call preventDefault() to handle insertion yourself. |
onError | (error: Error) => void | throw | Handles editor errors; errors are thrown when omitted. |
aria-label / aria-labelledby / aria-describedby / aria-invalid / aria-required | React 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.
<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.
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 @
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.
// 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
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>
</>
);
}| Method | Result |
|---|---|
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.