Rich Text Email Composer
Add a polished compose window with Scribe: inline formatting via editor.bold(), clean body output via getHTML(), signature editing with setHTML(), and paste-safe content from Word and Outlook — all under 50KB.
Everything an email composer needs
Inline formatting for messages
Bold, italic, links, lists, and blockquotes via the direct API — editor.bold(), editor.italic(), editor.link(url). Everything an email body needs without a heavyweight editor.
Clean HTML via getHTML()
Call editor.getHTML() to get clean, semantic HTML for the email body. No editor wrapper cruft, no proprietary classes — just the markup your mail transport needs.
Signature editing
Run a second Scribe instance for the saved signature. Load it with setHTML(), persist with getHTML(), and append it to the body before sending.
Paste-safe & XSS-safe
Users paste replies from Word, Outlook, and Google Docs. Scribe strips proprietary markup on paste and sanitizes HTML, so stored email bodies stay clean and safe.
Compose Window with Clean HTML Output
Wire inline formatting buttons to the direct API, then send the body via a single getHTML() call.
import { Scribe } from 'scribejs-editor';
import 'scribejs-editor/dist/scribe.css';
// A compose window: a fixed toolbar reads well for email bodies
const editor = Scribe.init('#email-body', {
toolbar: 'fixed',
autofocus: true,
placeholder: 'Write your message...',
});
// Apply inline formatting from your own toolbar buttons
document.querySelector('#bold').addEventListener('click', () => editor.bold());
document.querySelector('#italic').addEventListener('click', () => editor.italic());
document.querySelector('#link').addEventListener('click', () => {
const url = prompt('Link URL:');
if (url) editor.link(url);
});
// Send: grab clean HTML for the email body
document.querySelector('#send-btn').addEventListener('click', async () => {
const bodyHtml = editor.getHTML(); // sanitized, clean semantic HTML
await fetch('/api/email/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
to: document.querySelector('#to').value,
subject: document.querySelector('#subject').value,
html: bodyHtml,
}),
});
});Signature Editing
Run a second Scribe instance for the saved signature. Load it with setHTML() and persist with getHTML().
import { Scribe } from 'scribejs-editor';
// Separate editor instance for the user's saved signature
const signature = Scribe.init('#signature-editor', {
toolbar: 'floating',
placeholder: 'Add your email signature...',
});
// Load an existing signature from the server
const saved = await fetch('/api/settings/signature').then(r => r.json());
if (saved.html) signature.setHTML(saved.html);
// Persist signature HTML on save
document.querySelector('#save-sig').addEventListener('click', async () => {
const html = signature.getHTML();
await fetch('/api/settings/signature', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html }),
});
});
// When composing, append the signature to the body HTML before sending
function composeWithSignature(bodyHtml, signatureHtml) {
return `${bodyHtml}<br><br>--<br>${signatureHtml}`;
}React Email Composer Component
A complete React component that grabs sanitized HTML on send and appends the signature.
import { useRef, useEffect, useState } from 'react';
import { Scribe } from 'scribejs-editor';
export function EmailComposer({ onSend, signatureHtml }) {
const containerRef = useRef(null);
const editorRef = useRef(null);
const [sending, setSending] = useState(false);
useEffect(() => {
editorRef.current = Scribe.init(containerRef.current, {
toolbar: 'fixed',
autofocus: true,
placeholder: 'Write your message...',
});
return () => editorRef.current?.destroy();
}, []);
const send = async (to, subject) => {
const editor = editorRef.current;
if (!editor || editor.isEmpty()) return;
setSending(true);
// getHTML() returns sanitized HTML, safe to store and resend
const body = editor.getHTML();
const html = signatureHtml
? `${body}<br><br>--<br>${signatureHtml}`
: body;
try {
await onSend({ to, subject, html });
editor.setHTML(''); // clear the compose window after sending
} finally {
setSending(false);
}
};
return (
<div className="email-composer">
<div ref={containerRef} className="prose max-w-none" />
<button disabled={sending} onClick={() => send(/* to, subject */)}>
{sending ? 'Sending...' : 'Send'}
</button>
</div>
);
}Everything an email writer expects
Ship your email composer today.
Inline formatting, clean HTML bodies, signature editing, safe paste. Everything in 50KB — free under MIT.
Email composer — common questions
How do I build a rich text email composer in JavaScript?
Initialize Scribe Editor with toolbar: 'fixed' on your compose window, expose inline formatting with the direct API (editor.bold(), editor.italic(), editor.link(url)), and on send call editor.getHTML() to get clean HTML for the email body. Scribe is under 50KB gzipped with zero dependencies and works with React, Vue 3, Svelte, and Vanilla JS.
How do I get clean HTML for an email body from Scribe?
Call editor.getHTML(). It returns sanitized, semantic HTML without editor wrapper markup or proprietary class names. Send that string as the html field to your mail transport. Because Scribe sanitizes on input and paste, the HTML you store and resend stays clean.
Can I edit an email signature with Scribe Editor?
Yes. Run a second Scribe instance bound to a signature field. Load the saved signature with editor.setHTML(html) and persist edits with editor.getHTML(). When composing, append the signature HTML to the body before sending. A floating toolbar works well for the smaller signature surface.
Does Scribe handle pasting replies from Word or Outlook safely?
Yes. Scribe has built-in paste sanitization that strips proprietary Word, Outlook, and Google Docs markup and converts content to clean semantic HTML. Combined with built-in XSS sanitization, this keeps pasted reply content safe and consistent in your stored email bodies.
Also compare Scribe with:
Use Scribe in your framework:
Popular use cases: