If you are building a SaaS, CRM, or marketing platform, chances are your users need a way to design custom email templates directly within your application.
Building an in-house visual canvas that exports bulletproof, cross-client HTML (handling Outlook, Apple Mail, Gmail quirks, and nested tables) is an endless maintenance sinkhole. Existing embedded builder providers can often feel heavy, expensive, or complex to embed.
To solve this, we launched react-email-craft, a lightweight, embeddable React email builder SDK designed to drop directly into your modern web apps.
What is react-email-craft?
react-email-craft is an embeddable visual editor component that gives your end-users a clean drag-and-drop experience while outputting production-ready email markup:
- Lightweight & Fast: Runs in a sandboxed iframe runtime so your parent bundle stays lean, no editor code ships with your app.
- Responsive Preview: Real-time desktop and mobile canvas viewports.
- Production-Ready Output: Compiles clean, client-tested email HTML (MJML under the hood) ready to send via Resend, SendGrid, Postmark, or AWS SES.
- Familiar API: Drop-in ref methods (loadDesign, saveDesign, exportHtml), migration from react-email-editor / Unlayer is literally one changed import.
- Full TypeScript Support: Typed configs, props, and design interfaces out of the box.
- Framework siblings: Same editor also ships as vue-email-craft, angular-email-craft, and vanilla-email-craft.
Not sure if it fits? Open the live playground and drag some blocks around, no signup, no install. Or browse 48 ready-made templates to see what your users can start with.
Quick Start
1. Install the package
npm install react-email-craft
# or
pnpm add react-email-craft
2. Embed the Component
import React, { useRef } from "react";
import EmailEditor, { type MailkaroIframeEditorRef } from "react-email-craft";
export default function EmailBuilderView() {
const emailEditorRef = useRef<MailkaroIframeEditorRef>(null);
const handleExport = () => {
emailEditorRef.current?.editor.exportHtml((data) => {
const { html, design } = data as { html: string; design: unknown };
// Save the design JSON to your DB and send the HTML via your email API
console.log("Compiled HTML:", html);
console.log("Template JSON:", design);
});
};
const handleReady = () => {
// Editor is initialised; hydrate a saved template if you have one:
// emailEditorRef.current?.editor.loadDesign(savedDesignJson);
};
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
<header style={{ padding: "12px 16px", borderBottom: "1px solid #e2e8f0" }}>
<button onClick={handleExport}>Export Email</button>
</header>
<div style={{ flex: 1, minHeight: 0 }}>
<EmailEditor ref={emailEditorRef} onReady={handleReady} />
</div>
</div>
);
}
That's it, no backend, no account, no token needed to try it. Drag some blocks in, hit "Export Email", and you'll see production-ready HTML in your console.
How It Works
react-email-craft acts as the client-side bridge for the editor engine. The actual builder runs inside a sandboxed iframe hosted on MailKaro's CDN, which keeps your parent bundle tiny and isolates the editor's dependencies from your app.
When you call exportHtml, the editor sends your design JSON to a server-side renderer that compiles it to inbox-safe HTML (built on MJML internally), so you don't have to think about Outlook's VML quirks, Gmail's style stripping, or Apple Mail's dark-mode inversion. Only the design JSON and the pk_live_ token travel to the renderer, no recipient data or custom merge values are sent unless you explicitly pass them.
Paid features (white-labeling, custom blocks, AI assist, Google Drive / S3 integrations) are gated server-side with a pk_live_ publishable token:
<EmailEditor ref={emailEditorRef} token="pk_live_..." onReady={handleReady} />
The free tier gives you the full editor and exportHtml() without any token, you only need one when you want to unlock Pro features. Entitlements never ship to the browser, so your users can't bypass your plan.
Links
- ๐ฆ npm Package: https://www.npmjs.com/package/react-email-craft
- ๐จ Live Playground (try without installing): https://mailkaro.com/demo
- ๐ Free Workspace (sign up, no card required): https://mailkaro.com/signup
- ๐งฉ Template Library (48 ready-to-use templates): https://mailkaro.com/templates
- ๐ Documentation & Integration Guide: https://docs.mailkaro.com
- ๐ฐ Pricing (Free plan available, Pro from $60/mo): https://mailkaro.com/pricing
- ๐งฉ Framework Siblings: vue-email-craft ยท angular-email-craft ยท vanilla-email-craft ยท email-craft-sdk (Node backend)
If you are looking for an embeddable, customizable email builder for your React apps, give it a try and share your feedback or feature requests!
Top comments (3)
For the asynchronous export bridge, I'd test design A exporting while the user loads design B before the renderer responds. Does the callback identify the exact design revision that produced its HTML, so a save can't pair A's HTML with B's current JSON? A second fixture is a renderer timeout while the editor remains usable: show export failure explicitly and don't return the previous successful HTML as if it belongs to the new draft. Since export sends design JSON to a server, that processing boundary is also worth stating beside the "client-side bridge" description, including whether sample recipient data/custom merge values are included. Article-based suggestions, not results from running the SDK.
Hi @launchgatecheck
Quick update, shipped the fixes in react-email-craft@1.0.2 (just published):
1) A/B race on export
exportHtml / saveDesign / exportAmp / exportDocument / exportZip / exportPlainText now return a short exportId string and echo it on the callback payload. Hosts can also pass an opaque
tagoption that gets echoed back, so a host that already tracks revisions can correlate without inventing a new scheme. If the user loads design B before A's response comes back, the payload is identifiable.2) Renderer timeout + explicit errors
Added optional { onError, timeoutMs } to the export/save calls. On failure or timeout, onError fires. If no handler is passed, the error is rethrown on the microtask queue so it still shows up in dev tools instead of silently vanishing. We never fall back to a previously successful HTML.
3) Server-side processing disclosure
Updated the post to clarify that only the design JSON and the pk_live_ token travel to the renderer, no recipient data or custom merge values are sent unless the host explicitly passes them.
Everything is additive, no breaking changes. Install the update:
npm i react-email-craft@latest
Thanks again, that was a sharp read of the API surface.
Hi @launchgatecheck
Great points, these are exactly the right stress tests for an async export surface. Taking them one at a time:
1) A/B race on export
You're right โ the current exportHtml(callback) doesn't surface a revision id, so a host can't verify the HTML belongs to the design that was active when export was triggered. Internally the bridge uses promise-based correlation but we don't expose it on the payload. The fix is either returning a designRevision/exportId field alongside html and design, or documenting a cancel-token pattern. Logging this to tighten in the next minor.
2) Renderer timeout
Currently the callback fires only on success โ a slow renderer leaves the user in limbo without a clear signal, and silently returning a prior successful HTML would be exactly the wrong behavior. We should surface an explicit error path (timeout + onExportError) and never fall back to cached HTML for a different design state. Adding this to the list.
3) "Client-side bridge" vs server processing
Fair call-out โ I conflated the editor runtime (client-side, sandboxed iframe) with export, which does hit a server-side renderer (MJML pipeline). To clarify what travels: only the design JSON and the pk_live_ token for entitlement checks are sent โ sample recipient data and custom merge values stay client-side unless you explicitly pass them as render-time variables.
Thanks for taking the time to think through the failure modes โ this is the kind of feedback that actually improves the SDK. Will tighten the post for #3 and open issues for #1 and #2.