@abduljebar/text-editor
v3.0.1
Published
A professional React text editor with export, save, and validation features
Maintainers
Readme
@abduljebar/text-editor
A clean, minimal React rich-text editor inspired by Asana's text editor. Ships in two modes: a simple 8-button toolbar by default, and an advanced 28-button toolbar when you need full formatting power.
Features
Simple mode (default)
- Bold, Italic, Underline, Strikethrough
- Heading (H1)
- Bullet list, Numbered list
- Link insertion
- Clean, focus-preserving toolbar (editor never loses selection on button click)
Advanced mode (advanced prop)
- Everything in simple mode, plus:
- H2, Paragraph, Code block, Blockquote
- Indent / Outdent
- Align Left / Center / Right
- Superscript, Subscript
- Image upload (drag & drop, file picker, paste)
- Save / Export / Clear action buttons
- Unsaved changes indicator
Always included
- Correct word and character count (HTML tags excluded)
- Placeholder that correctly reappears after clearing
- Debounced
onChange - Read-only mode
- Ref API for programmatic control
- TypeScript types
Installation
npm install @abduljebar/text-editor
# or
yarn add @abduljebar/text-editor
# or
pnpm add @abduljebar/text-editorQuick Start
import '@abduljebar/text-editor/dist/index.css';
import { TextEditor } from "@abduljebar/text-editor";
function App() {
return (
<TextEditor
height="500px"
onChange={(html) => console.log(html)}
/>
);
}Usage
Simple mode
import '@abduljebar/text-editor/dist/index.css';
import { TextEditor } from "@abduljebar/text-editor";
function MyEditor() {
return (
<TextEditor
height="500px"
placeholder="Write something..."
initialContent="<p>Hello world</p>"
onChange={(html) => {
// called on every change, debounced by 300ms
console.log(html);
}}
/>
);
}Advanced mode
import '@abduljebar/text-editor/dist/index.css';
import { TextEditor } from "@abduljebar/text-editor";
function AdvancedEditor() {
const handleImageUpload = async (file: File): Promise<string> => {
const formData = new FormData();
formData.append('image', file);
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const data = await res.json();
return data.url;
};
return (
<TextEditor
advanced
showButtons
showStatusBar
height="600px"
onSave={(html) => console.log('saved:', html)}
onExport={(html) => console.log('exported:', html)}
onImageUpload={handleImageUpload}
onChange={(html) => console.log('changed:', html)}
/>
);
}Read-only mode
<TextEditor
readOnly
initialContent="<h1>Published Article</h1><p>This content cannot be edited.</p>"
showStatusBar
/>API Reference
TextEditor Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| initialContent | string | "" | Initial HTML content |
| onChange | (html: string) => void | undefined | Called on every content change (debounced) |
| readOnly | boolean | false | Disables editing |
| height | string | "500px" | Editor height — any valid CSS value (e.g. "400px", "100%") |
| className | string | "" | Extra CSS class on the root element |
| placeholder | string | "Start typing here..." | Shown when editor is empty |
| autoFocus | boolean | false | Focus editor on mount |
| onInit | (editor: HTMLDivElement) => void | undefined | Called after editor mounts |
| debounceDelay | number | 300 | onChange debounce delay in ms. Set to 0 to disable |
| showStatusBar | boolean | false | Show word/character count bar |
| advanced | boolean | false | Enable 28-button advanced toolbar |
| showButtons | boolean | false | Show Save / Export / Clear buttons (advanced mode only) |
| onSave | (html: string) => void | undefined | Called when Save is clicked (advanced mode) |
| onExport | (html: string) => void | undefined | Called when Export is clicked; also triggers .html download (advanced mode) |
| onImageUpload | (file: File) => Promise<string> | undefined | Upload handler — receives the file, returns the final URL (advanced mode). If omitted, images are embedded as data URLs |
| allowedImageTypes | string[] | ["image/jpeg","image/png","image/gif","image/webp","image/svg+xml"] | Accepted image MIME types (advanced mode) |
| maxImageSize | number | 5242880 (5 MB) | Max image size in bytes (advanced mode) |
TextEditorRef
Access the ref API with useRef<TextEditorRef>():
interface TextEditorRef {
getContent: () => string; // raw innerHTML of the editor
getHTML: () => string; // same as getContent
clear: () => void; // clears all content
focus: () => void; // focuses editor, cursor at end
insertText: (text: string) => void;
insertHTML: (html: string) => void;
setHTML: (html: string) => void; // replace all content programmatically
executeCommand: (command: string, value?: string) => void;
}Example:
import { useRef } from 'react';
import { TextEditor, TextEditorRef } from "@abduljebar/text-editor";
function EditorWithRef() {
const editorRef = useRef<TextEditorRef>(null);
return (
<>
<TextEditor ref={editorRef} height="400px" />
<button onClick={() => editorRef.current?.executeCommand('bold')}>
Bold
</button>
<button onClick={() => {
const html = editorRef.current?.getHTML();
console.log(html);
}}>
Get HTML
</button>
<button onClick={() => editorRef.current?.clear()}>
Clear
</button>
<button onClick={() => editorRef.current?.setHTML('<p>New content</p>')}>
Reset
</button>
</>
);
}Supported Commands (executeCommand)
| Command | Value | Description |
|---------|-------|-------------|
| bold | — | Toggle bold |
| italic | — | Toggle italic |
| underline | — | Toggle underline |
| strikeThrough | — | Toggle strikethrough |
| formatBlock | "h1", "h2", "p", "pre", "blockquote" | Change block type |
| insertUnorderedList | — | Toggle bullet list |
| insertOrderedList | — | Toggle numbered list |
| indent | — | Indent |
| outdent | — | Outdent |
| justifyLeft | — | Align left |
| justifyCenter | — | Align center |
| justifyRight | — | Align right |
| superscript | — | Superscript |
| subscript | — | Subscript |
| createLink | URL string | Insert/wrap link |
| insertImage | URL string | Insert image |
| undo | — | Undo |
| redo | — | Redo |
Styling
Import the bundled CSS before using the component:
import '@abduljebar/text-editor/dist/index.css';Override styles with the className prop:
<TextEditor className="my-editor" />.my-editor {
border: 2px solid #4f46e5;
border-radius: 12px;
}
/* Target the contenteditable area */
.my-editor .editor-content h1 {
color: #4f46e5;
}Integration with form libraries
import { useForm } from 'react-hook-form';
import { TextEditor } from "@abduljebar/text-editor";
function ArticleForm() {
const { handleSubmit, setValue } = useForm<{ body: string }>();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<TextEditor
height="400px"
onChange={(html) => setValue('body', html)}
/>
<button type="submit">Submit</button>
</form>
);
}Browser Support
Chrome 60+, Firefox 55+, Safari 12+, Edge 79+
Troubleshooting
| Problem | Fix |
|---------|-----|
| Styles missing | Import @abduljebar/text-editor/dist/index.css |
| Toolbar loses selection on click | Update to v3.0.0+ — fixed via onMouseDown approach |
| Word count includes HTML tags | Update to v3.0.0+ — fixed, count is text-only |
| Placeholder doesn't reappear after clear | Update to v3.0.0+ — fixed, placeholder is based on text content |
| onChange fires with wrong signature | Update call to (html: string) — title and content params removed in v3.0.0 |
Changelog
v3.0.0
- Breaking:
onChangesignature simplified from(content, html, title?)to(html) - Breaking: removed
showSaveTitle,imageUploadEndpointprops - New:
advancedprop — opt into full 28-button toolbar - Fix: toolbar buttons now use
onMouseDown+e.preventDefault()— editor never loses selection on click - Fix: active-format detection replaced stale-closure pattern with single atomic state update
- Fix: word/character count now strips HTML before counting
- Fix: placeholder correctly reappears when editor is cleared to
<br>or<div><br></div> - Fix: external
initialContentprop changes no longer blocked by user edits - Fix:
react/react-domremoved fromdependencies(were duplicating peer deps) - Removed: lodash runtime dependency
v2.8.0
- Added
setHTMLref API - Refactored component styles with modular CSS classes
v2.x
- Image upload with drag & drop, paste, file picker
- Pending images tracking
- Export to HTML with embedded styles
- Ref API for programmatic control
v1.0.0
- Initial release
Built with by AbdulJebar Sani
