react-email-craft
v1.3.0
Published
React email editor and React email template builder, an embeddable, drag-and-drop WYSIWYG for modern web applications.
Maintainers
Keywords
Readme
React Email Editor & React Email Template Builder, react-email-craft
An embeddable React email editor and React email template builder for modern web applications.
Drop-in replacement for react-email-editor (Unlayer) and a lightweight alternative to Beefree.
Install one small package, drop in one component, and your users get a full visual email template
builder with a familiar ref API.
- Lightweight, a tiny install, so your app stays fast.
- Drop-in, props and
refmethods matchreact-email-editor, so switching over is easy. - Secure, your plan and features are controlled by your token; nothing secret ships to the browser.
- Multi-framework, this is the React component; sibling packages ship for Vue, Angular, and vanilla JS (all thin iframe wrappers via the exported
mountEditor/connectHostBridge). White-label from the Pro plan.
Try it live
Open the Next.js starter in your browser, no setup:
Comparing options? MailKaro vs Unlayer · vs GrapesJS · vs Beefree SDK · Guide: build an email builder in React + Node.js
Requirements
- React 18 or newer (React is a peer dependency, you already have it).
- A publishable token that looks like
pk_live_.... You get this from your Mailkaro dashboard (or your Mailkaro provider gives you one). It is safe to ship in your frontend, it carries no secret and only works on the website domains you allow-list for it.
Install
npm install react-email-craft
# or: pnpm add react-email-craft / yarn add react-email-craftQuick start
import EmailEditor from "react-email-craft";
import { useRef } from "react";
export function MyEditor() {
const ref = useRef(null);
// Ask the editor for the final HTML (e.g. when the user clicks "Save").
const handleSave = () => {
ref.current.editor.exportHtml(({ design, html }) => {
console.log("design JSON:", design); // save this to re-open the email later
console.log("email HTML:", html); // send this through your email provider
});
};
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
<button onClick={handleSave}>Save</button>
<EmailEditor
ref={ref}
token="pk_live_xxxxxxxxxxxxx"
style={{ flex: 1 }}
/>
</div>
);
}That's it, you now have a working email builder. Everything below is optional.
Coming from react-email-editor
Change the package and the import, nothing else. The same names are exported, your saved Unlayer designs load as they are, and the ref, props and events behave the same:
import EmailEditor, { type EditorRef, type EmailEditorProps } from "react-email-craft";Using Next.js (App Router)
The editor runs in the browser, so render it on the client. Either add "use client" at the top of
your file:
"use client";
import EmailEditor from "react-email-craft";…or import it with server-side rendering turned off:
import dynamic from "next/dynamic";
const EmailEditor = dynamic(() => import("react-email-craft"), { ssr: false });Common things you'll want to do
1. Re-open a saved email
When a user saves, you store the design JSON. To let them edit it again later, load it back once the editor is ready:
<EmailEditor
ref={ref}
token="pk_live_..."
onReady={() => ref.current.editor.loadDesign(savedDesignJson)}
/>You can also pass it up-front with the design prop: <EmailEditor design={savedDesignJson} … />.
2. Get the email out (HTML, plain text, ZIP, etc.)
ref.current.editor.exportHtml(({ html }) => sendEmail(html)); // HTML
ref.current.editor.exportPlainText(({ text }) => console.log(text)); // plain-text version
ref.current.editor.saveDesign((design) => save(design)); // just the design JSON3. Let users upload their own images to your storage
When a user adds an image, Mailkaro hands you the file so you can upload it wherever you want (S3, Cloudinary, your server…). You return the final URL:
<EmailEditor
token="pk_live_..."
onImageUpload={async (file) => {
const url = await uploadToMyStorage(file); // your code
return { url }; // (optionally { url, alt })
}}
/>4. Add merge tags (personalization like {{first_name}})
<EmailEditor
token="pk_live_..."
mergeTags={{
first_name: { name: "First name", value: "{{first_name}}" }, company: { name: "Company", value: "{{company}}" }, }}
/>5. Know when the design changes (e.g. to auto-save)
<EmailEditor token="pk_live_..." onDesignChange={(design) => autosave(design)} />6. Build a PDF, letter, landing page, or popup (not just email)
displayMode tells the editor what kind of thing you're building. The canvas, defaults, and preview change to match.
| You're building | Set displayMode to | What changes |
| --- | --- | --- |
| An email (default) | "email" or leave it out | Email-width canvas (~600px), inbox preview on top |
| A PDF or printed letter | "document" | Page-sized canvas (~US Letter), no inbox preview, exported HTML is a full page |
| A landing page / web page | "web" | Wider canvas (~960px) |
| A popup / modal | "popup" | Narrow modal-width canvas |
// PDF or letter
<EmailEditor token="pk_live_..." displayMode="document" />
// Landing page
<EmailEditor token="pk_live_..." displayMode="web" />If you keep displayMode="email" while building a PDF or letter, pass inboxPreview={false} to hide the Gmail / Apple Mail preview strip.
All props
| Prop | Type | What it does |
| --- | --- | --- |
| token | string | Your pk_live_... publishable token. Unlocks your plan's features. |
| design | object | A saved design JSON to load when the editor opens. |
| mergeTags | object | Personalization tags shown in the editor (see example above). |
| onImageUpload | (file) => Promise<{ url, alt? }> | Runs on your page when a user adds an image, upload it and return the URL. With an editor.registerCallback("image", ...) handler (the Unlayer way), that handler uploads and this prop is react-email-editor's onImageUpload event: it gets { image: { url } } after each upload. |
| onReady | (editor) => void | Fires when the editor has finished loading. Receives the editor, like react-email-editor. |
| onLoad | (editor) => void | Fires when the editor is created, before onReady. Receives the editor; calls made now are queued until it is ready. |
| onError | (error) => void | The editor is not running as it was set up. Receives { code, message }: invalid_embed_token, domain_not_allowed, token_expired or runtime_unreachable (the editor then runs on the free plan), or editor_not_started (the frame did not answer in 20 seconds: a wrong embedUrl, or the frame was refused). Also the error event. Show message as plain text. |
| options | object | A react-email-editor style options object. Reads mergeTags, displayMode, locale and fonts (a list or Unlayer's { customFonts }); other keys are accepted and ignored. A top-level prop wins over the same key in options. |
| editorId | string | Id of the container the editor renders into, like react-email-editor. |
| onDesignLoad, onDesignUpdated | (payload) => void | Unlayer-style handlers. onDesignUpdated is the same event as onDesignChange. |
| on... | (payload) => void | Any other on... prop is attached with editor.addEventListener, as in react-email-editor. |
| projectId, tools, appearance, scriptUrl | | Deprecated. Accepted so migrated code compiles; Mailkaro takes the project from token and loads a hosted frame, not a script. |
| onDesignChange | (design) => void | Fires whenever the user edits the design. |
| fonts | Array<{ label, value, url? }> | Extra web fonts to offer in the font picker. |
| displayMode | "email" \| "web" \| "popup" \| "document" | What kind of thing you're building. Defaults to email. |
| inboxPreview | boolean | Set false to hide the Gmail / Apple Mail preview when you are building a PDF or a letter that is not an email. Defaults to true. Also read from options.inboxPreview. |
| locale | string | UI language, e.g. "en", "hi". |
| s3 | object | Turn on the S3 asset tab (see email-craft-sdk for the secure setup). |
| googleDrive | object \| boolean | Turn on the "Connect Google Drive" picker. |
| project | string | Optional project/workspace id for multi-project accounts. |
| runtimeUrl | string | Advanced: instead of a token, point at your backend endpoint that returns the runtime (see email-craft-sdk). |
| apiBaseUrl | string | Advanced: override the Mailkaro API URL. |
| embedUrl | string | Advanced: override the editor service URL (for local development). |
| style, className | — | Applied to the container the editor fills. |
| minHeight | string \| number | Minimum editor height. Defaults to 500px, like react-email-editor. The editor also grows to fill a flex parent. |
Editor methods (ref.current.editor.*)
Calls made before the editor is ready (from onLoad, or straight after mounting) are queued and run once it is.
onLoad, onReady and ref.current.editor all hand you the same editor object, like react-email-editor.
| Method | What it does |
| --- | --- |
| loadDesign(design) | Load a saved design JSON (Unlayer's or ours) into the editor. |
| loadBlank(options?) | Start a fresh, empty email. |
| saveDesign(cb) | Get the current design JSON: saveDesign(d => …). |
| exportHtml(cb, options?) | Get { design, html, chunks, amp } (Unlayer's shape). options.mergeTags puts values into the HTML. exportLiveHtml is the same. Without a callback it returns a promise (await editor.exportHtml()), as do the other exports, saveDesign, canUndo/canRedo and audit. |
| exportPlainText(cb) | Get the plain-text version. |
| exportAmp(cb) | Get the AMP version. |
| exportDocument(cb) | Get a document-style export. |
| exportZip(cb) | Get a ZIP (HTML + assets). |
| setMergeTags(tags) | Replace the merge tags at runtime. |
| setBodyValues(values) | Set body-level settings (e.g. background). |
| showPreview(device?) / hidePreview() | Toggle the preview overlay. |
| addEventListener(event, handler) / removeEventListener(event, handler?) | Listen for design:updated (edits, with Unlayer's change type), design:loaded ({ design }), image:uploaded, editor:ready. Like Unlayer, one listener per event name: adding another replaces it. |
| registerCallback("image", (file, done) => …) | Upload images yourself. file is the File and also has Unlayer's attachments/accepted; call done({ progress }) while uploading and done({ progress: 100, url }) at the end. A failed upload: done({ error: "message" }) or throw, and the editor shows the message. unregisterCallback("image") removes it. |
| registerCallback("selectImage", (data, done) => …) | Your own image library: "Open Library" on an image or GIF calls it with { kind }; call done({ url, alt? }) with the chosen image, or done({}) if nothing was chosen. |
| registerCallback("previewHtml", (params, done) => …) | Change what Preview shows: params.html has the merge tags as written; call done({ html }). Exports are not affected. |
| undo() / redo() / canUndo(cb) / canRedo(cb) | The editor's undo history. |
| audit(cb) / setValidator(fn) | The editor's checks (links, alt text, accessibility) as { status, errors }, plus your own validator. |
| setLocale(locale) / setDisplayMode(mode) / setAppearance({ theme, customCSS }) / setTextDirection(dir) | Change these at runtime. |
| exportImage(cb) / exportPdf(cb) | Answer with { design, url: null, error } (they need a server render: use email-craft-sdk). |
| registerTool(config) / createViewer({ render }) | Your own block, in Unlayer's shape (name, label, icon, options groups with widgets, values, renderer: { Viewer, exporters }) or MailKaro's (id, label, values, inspector, exportHtml). It shows in the Content panel. Its code runs on your page: the editor asks it for a block's HTML when the values change and keeps that HTML in the block (values._html), so exports never wait for it. Returns a promise. See Custom tools. |
| setDisplayConditions(list) and the displayConditions option | Unlayer's display conditions: a row's settings get a picker, and the chosen before / after go around the row in the export. Saved Unlayer designs keep theirs. |
| options.features.textEditor.customButtons | Unlayer's text toolbar buttons: onAction({ text }, done) runs on your page with the selected words. |
| Other Unlayer methods (setDesignId, registerProvider, setStyleGuide, ...) | Accepted so code written for Unlayer runs; they have no effect here. |
Backend companion (optional but recommended)
If you want features like your own S3, your own AI key, or to keep entitlement checks on your
server, install the backend SDK email-craft-sdk.
Your backend holds the secret sk_live_... key and returns a "runtime" to your frontend; then you use
runtimeUrl="/your/endpoint" on the editor instead of a token. Full guide in that package.
Troubleshooting
- Editor area is blank / won't load → your site's domain probably isn't in the token's allow-list.
Add your domain (e.g.
https://app.yoursite.com) to the token in your Mailkaro dashboard. - Features are locked / it looks "free" → the
tokenis missing, wrong, or expired. A missing or invalid token safely falls back to the free feature set. ref.current.editoris undefined → call editor methods afteronReadyfires (the editor needs a moment to initialize).- Images don't stick → make sure
onImageUploadreturns{ url }with a public URL.
License
MIT
