RichTextEditor
AppQuill 2.x WYSIWYG editor with token-tinted snow theme. Production features: controlled value, imperative ref API, onBlur, name+hidden-form-sync, drag-and-drop image upload with onImageUpload callback, paste sanitization (Word / GDocs cleanup), char + word counter with maxLength, autosave to localStorage, markdown shortcuts, color + highlight, sub/sup, indent/outdent, horizontal rule, tables, emoji picker, @-mentions, /-slash command menu, selection bubble menu, image resize/align overlay, fullscreen mode. Pixel-identical EJS sibling at modules/app/RichTextEditor.ejs.
Use the toolbar — markdown shortcuts work too: try **bold** or # heading.
const [body, setBody] = useState('');
<RichTextEditor
id="article-body"
label="Article body"
value={body}
onChange={setBody}
showCounter
showWordCount
/><RichTextEditor
id="release-notes"
label="Release notes"
defaultValue={initial}
onChange={setBody}
/>This document is read-only.
<RichTextEditor
id="archived"
defaultValue={savedHtml}
readOnly
/>Maximum 200 characters.
<RichTextEditor
id="short-summary"
maxLength={200}
showCounter
/>Type @ to mention a teammate, or / to insert a block.
<RichTextEditor
id="comment"
mentions={[
{ id: 'u1', label: 'Jane Doe', description: 'Designer' },
{ id: 'u2', label: 'John Smith', description: 'Engineer' },
]}
slashItems={[
{ id: 'h1', label: 'Heading 1', action: (q) => q.format('header', 1, 'user') },
{ id: 'list', label: 'Bullet list', action: (q) => q.format('list', 'bullet', 'user') },
]}
/>const ref = useRef<RichTextEditorHandle>(null);
<RichTextEditor ref={ref} id="reply" />
ref.current?.focus();
ref.current?.clear();
ref.current?.insertHTML('<p>Hi!</p>');
const text = ref.current?.getText();<form action="/api/post" method="post">
<RichTextEditor id="post" name="body" defaultValue={savedHtml} />
<button type="submit">Save</button>
</form>Your changes survive page refresh — key: 'demo-draft'.
<RichTextEditor
id="draft"
autosaveKey="my-draft"
placeholder="Survives page refresh…"
/>