@draftpad/rich-editor
@draftpad/rich-editor
A portable, headless-ready rich-text editor for React. Built on contenteditable + the Selection/Range API — no ProseMirror, no Quill, no heavy runtime dependency.
- Full formatting toolbar (bold, italic, headings, lists, code, alignment, colors, font)
- Find & Replace with live highlighting
- Hyperlink insert/remove dialog
- Ctrl+F / Ctrl+K / Ctrl+P keyboard shortcuts
- Imperative
insertTexthandle (great for AI text injection) - CSS custom properties for zero-friction theming
- Tree-shakeable: use
RichEditoras a complete solution, or import individual pieces
Installation
npm install @draftpad/rich-editor
# peer deps (install if not already present)
npm install react react-dom lucide-reactQuick start
Next.js (App Router)
Import the stylesheet once in your root layout:
// app/layout.tsx
import '@draftpad/rich-editor/styles';Then use the editor in any client component:
'use client';
import { useState } from 'react';
import { RichEditor } from '@draftpad/rich-editor';
export default function MyEditor() {
const [html, setHtml] = useState('');
return (
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<RichEditor content={html} onChange={setHtml} />
</div>
);
}The package outputs
"use client"at the top of every bundle file, so Next.js automatically treats all exports as client components.
Plain React (Vite / CRA)
// main.tsx
import '@draftpad/rich-editor/styles';import { useState } from 'react';
import { RichEditor } from '@draftpad/rich-editor';
function App() {
const [html, setHtml] = useState('');
return <RichEditor content={html} onChange={setHtml} />;
}API
<RichEditor>
The all-in-one component: toolbar + find bar + editor canvas + link dialog.
import { RichEditor, RichEditorHandle } from '@draftpad/rich-editor';Props
| Prop | Type | Default | Description |
|---|---|---|---|
content |
string |
— | Required. HTML string shown in the editor. Update this from outside to push remote changes (e.g. Yjs). |
onChange |
(html: string) => void |
— | Required. Called whenever the user edits the content. |
readOnly |
boolean |
false |
Renders a read-only view; hides the toolbar. |
placeholder |
string |
'Start writing…' |
Placeholder shown when the editor is empty. |
onAIToggle |
() => void |
— | Called when the user clicks the AI (✦) toolbar button. Wire this to open your AI panel. |
onTextSelect |
(text: string) => void |
— | Called on every selection change with the currently selected plain text. |
Ref handle (RichEditorHandle)
Use ref to imperatively insert text at the current cursor position — useful for injecting AI-generated content.
import { useRef } from 'react';
import { RichEditor, RichEditorHandle } from '@draftpad/rich-editor';
function Page() {
const editorRef = useRef<RichEditorHandle>(null);
const handleAIResult = (text: string) => {
editorRef.current?.insertText(text);
};
return (
<RichEditor
ref={editorRef}
content={html}
onChange={setHtml}
onAIToggle={() => setAIPanelOpen(true)}
/>
);
}| Method | Signature | Description |
|---|---|---|
insertText |
(text: string) => void |
Inserts plain text at the current cursor position and triggers onChange. |
Keyboard shortcuts
| Shortcut | Action |
|---|---|
Ctrl / Cmd + B |
Bold |
Ctrl / Cmd + I |
Italic |
Ctrl / Cmd + U |
Underline |
Ctrl / Cmd + K |
Insert / edit hyperlink |
Ctrl / Cmd + F |
Open Find & Replace bar |
Ctrl / Cmd + P |
|
Ctrl / Cmd + Z |
Undo |
Ctrl / Cmd + Y |
Redo |
Theming
Import the stylesheet once and override any --re-* variable in :root:
/* your-app.css */
@import '@draftpad/rich-editor/styles'; /* or import in JS/TS */
:root {
/* Colors */
--re-text-color: #111827; /* body text */
--re-heading-color: #111827; /* h1 / h2 / h3 */
--re-placeholder-color: #9ca3af; /* placeholder hint */
--re-link-color: #6366f1; /* anchor tags */
--re-accent-color: #6366f1; /* blockquote border, focus rings */
--re-blockquote-color: #6b7280; /* blockquote text */
--re-code-bg: #f3f4f6; /* inline code & pre background */
--re-code-color: #4f46e5; /* inline code text */
/* Typography */
--re-font-family: Inter, system-ui, sans-serif;
--re-mono-font: 'JetBrains Mono', 'Fira Code', monospace;
--re-font-size: 16px;
--re-line-height: 1.8;
}Dark theme example
:root {
--re-text-color: #F4F4F5;
--re-heading-color: #F4F4F5;
--re-placeholder-color: #52525B;
--re-link-color: #818CF8;
--re-accent-color: #6366F1;
--re-blockquote-color: #A1A1AA;
--re-code-bg: #1F1F23;
--re-code-color: #A5B4FC;
}The defaults ship as light-theme values, so you get a working editor with zero config on any white-background app.
Supported format commands
The toolbar exposes these commands via onFormat(format, value?).
If you build a custom toolbar using useEditorFormat, these are the strings to pass:
| Command | Value | Description |
|---|---|---|
bold |
— | Toggle bold |
italic |
— | Toggle italic |
underline |
— | Toggle underline |
strikethrough |
— | Toggle strikethrough |
superscript |
— | Toggle superscript |
subscript |
— | Toggle subscript |
clearFormat |
— | Remove all inline formatting |
undo |
— | Undo |
redo |
— | Redo |
h1 |
— | Format block as <h1> |
h2 |
— | Format block as <h2> |
h3 |
— | Format block as <h3> |
bullet |
— | Unordered list |
ordered |
— | Ordered list |
quote |
— | Blockquote |
code |
— | Wrap selection in <code> |
alignLeft |
— | Left-align |
alignCenter |
— | Center |
alignRight |
— | Right-align |
alignJustify |
— | Justify |
foreColor |
'#rrggbb' |
Text color (uses Range API — reliable across browsers) |
hiliteColor |
'#rrggbb' or 'transparent' |
Highlight / background color |
fontName |
e.g. 'Georgia, serif' |
Change font family |
fontSize |
e.g. '18px' |
Change font size |
link |
— | Open link dialog (requires onOpenLink wired up) |
Using individual components
All sub-components are exported separately for custom layouts.
<EditorToolbar>
import { EditorToolbar } from '@draftpad/rich-editor';
<EditorToolbar
onFormat={(format, value) => { /* call handleFormat */ }}
onAI={() => { /* open AI panel */ }}
onFindOpen={() => { /* open find bar */ }}
formatState={formatState} // from useEditorFormat
isViewer={false} // hides toolbar when true
/><FindReplaceBar>
import { FindReplaceBar } from '@draftpad/rich-editor';
<FindReplaceBar
editorRef={editorRef} // RefObject<HTMLDivElement | null>
isHighlighting={isHighlighting} // { current: boolean } — mutable ref
onClose={() => setShowFind(false)}
/><LinkDialog>
import { LinkDialog } from '@draftpad/rich-editor';
<LinkDialog
initialUrl="https://example.com" // pre-fills if editing an existing link
onConfirm={(url) => applyLink(url)}
onRemove={() => removeLink()}
onClose={() => setShowLink(false)}
/>useEditorFormat hook
Use this when you want full control over the editor DOM and toolbar layout but don't want to rewrite selection management and execCommand wiring.
import { useRef } from 'react';
import { useEditorFormat, EditorToolbar } from '@draftpad/rich-editor';
function MyCustomEditor() {
const editorRef = useRef<HTMLDivElement>(null);
const [showLink, setShowLink] = useState(false);
const [linkInitial, setLinkInitial] = useState('');
const { formatState, handleFormat, handleEditorBlur, applyLink, removeLink } =
useEditorFormat({
editorRef,
onContentChange: () => {
onChange(editorRef.current?.innerHTML ?? '');
},
onOpenLink: (initialUrl) => {
setLinkInitial(initialUrl);
setShowLink(true);
},
});
return (
<>
<EditorToolbar
onFormat={handleFormat}
onAI={() => {}}
onFindOpen={() => {}}
formatState={formatState}
/>
<div
ref={editorRef}
contentEditable
suppressContentEditableWarning
onBlur={handleEditorBlur}
className="editor-canvas"
/>
{showLink && (
<LinkDialog
initialUrl={linkInitial}
onConfirm={(url) => { applyLink(url); setShowLink(false); }}
onRemove={() => { removeLink(); setShowLink(false); }}
onClose={() => setShowLink(false)}
/>
)}
</>
);
}useEditorFormat options
| Option | Type | Description |
|---|---|---|
editorRef |
RefObject<HTMLDivElement | null> |
Ref attached to your contenteditable div. |
onContentChange |
() => void |
Called after every formatting operation that mutates the DOM. |
onOpenLink |
(initialUrl: string) => void |
Called when the user triggers the link command. Open your link dialog here. |
useEditorFormat return value
| Value | Type | Description |
|---|---|---|
formatState |
FormatState |
Current selection's font, size, text color, highlight color. |
handleFormat |
(format: string, value?: string) => void |
Applies a format command. |
handleEditorBlur |
() => void |
Saves selection on blur. Attach to onBlur of your contenteditable. |
applyLink |
(url: string) => void |
Creates a hyperlink from the saved selection. |
removeLink |
() => void |
Removes the hyperlink at the saved selection. |
FormatState
interface FormatState {
fontFamily: string; // current font family at cursor
fontSize: string; // current font size at cursor
textColor: string; // current text color at cursor
hlColor: string; // current highlight color at cursor
}TypeScript
All types are bundled. No @types/ package needed.
import type {
RichEditorProps,
RichEditorHandle,
FormatState,
} from '@draftpad/rich-editor';Requirements
| Peer dependency | Version |
|---|---|
react |
>= 18 |
react-dom |
>= 18 |
lucide-react |
>= 0.300.0 |
License
MIT — DraftPad