RichTextEditor

App

Quill 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.

Empty + counter

Preview
0 words0

Use the toolbar — markdown shortcuts work too: try **bold** or # heading.

Code
const [body, setBody] = useState('');
<RichTextEditor
  id="article-body"
  label="Article body"
  value={body}
  onChange={setBody}
  showCounter
  showWordCount
/>

Pre-populated

Preview
Code
<RichTextEditor
  id="release-notes"
  label="Release notes"
  defaultValue={initial}
  onChange={setBody}
/>

Read-only

Preview

This document is read-only.

Code
<RichTextEditor
  id="archived"
  defaultValue={savedHtml}
  readOnly
/>

Max length (200 chars)

Preview
0 / 200

Maximum 200 characters.

Code
<RichTextEditor
  id="short-summary"
  maxLength={200}
  showCounter
/>

Mentions + slash commands

Preview

Type @ to mention a teammate, or / to insert a block.

Code
<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') },
  ]}
/>

Imperative ref API

Preview
Code
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 integration

Preview
Code
<form action="/api/post" method="post">
  <RichTextEditor id="post" name="body" defaultValue={savedHtml} />
  <button type="submit">Save</button>
</form>

Autosave

Preview

Your changes survive page refresh — key: 'demo-draft'.

Code
<RichTextEditor
  id="draft"
  autosaveKey="my-draft"
  placeholder="Survives page refresh…"
/>
Sourcemodules/app/RichTextEditor/index.tsx