DEV Community

Cover image for Introducing react-email-craft: A Drop-in Visual Email Template Builder for React
Mailkaro
Mailkaro

Posted on

Introducing react-email-craft: A Drop-in Visual Email Template Builder for React

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

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

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)

Collapse
 
launchgatecheck profile image
Launch Gate •

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.

Collapse
 
mailkaro profile image
Mailkaro •

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 tag option 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.

Collapse
 
mailkaro profile image
Mailkaro •

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.