DEV Community

Cover image for Add a drag and drop email builder to your React app (full tutorial)
Mailkaro
Mailkaro

Posted on

Add a drag and drop email builder to your React app (full tutorial)

Your users want to design their own emails. You don't want to spend months building an editor.

In this post we add a complete drag and drop email builder to a fresh Vite + React app, step by step. It's the written version of a 9 minute video where every step is filmed for real.

🎥 Watch the video: https://www.linkedin.com/feed/update/urn:li:activity:7519114078284562432

What you need: a React app (ours is a fresh Vite app) and Node 18 or newer.

1. Create an account and a token

Sign up at mailkaro.com. Sign in is passwordless: every sign in uses a one time code sent to your email, and an account can only be signed in on one device at a time.

Then open Embed & API, click Generate new token, and choose which features the token exposes, for example:

  • Save, export and history
  • Uploads and assets
  • AI Assistant

You get two keys:

Key Starts with Where it goes
Publishable pk_live_ your frontend
Secret sk_live_ your server only, never in frontend code

2. Install and render

npm install react-email-craft
Enter fullscreen mode Exit fullscreen mode

Put the publishable key in .env.local:

VITE_MAILKARO_KEY=pk_live_...
Enter fullscreen mode Exit fullscreen mode

Then render the editor:

import EmailEditor from "react-email-craft";

export default function App() {
  return (
    <EmailEditor
      token={import.meta.env.VITE_MAILKARO_KEY}
      style={{ height: "100vh" }}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

That's the full editor: the canvas in the middle, blocks and settings on the right.

Tip: Vite's starter index.css centres everything in a narrow box. Replace it with body { margin: 0; } so the editor gets the whole page.

Next.js: add "use client" at the top of the file, or import the editor with ssr: false.

3. Save and reopen a design

Add a ref and two buttons. exportHtml hands you the design (JSON describing everything on the canvas) and the HTML. Store the design: that's what reopens the email later.

import { useRef } from "react";
import EmailEditor from "react-email-craft";

export default function App() {
  const ref = useRef(null);

  const save = () =>
    ref.current.editor.exportHtml(({ design, html }) => {
      localStorage.setItem("design", JSON.stringify(design)); // your database in a real app
    });

  const open = () => {
    const saved = localStorage.getItem("design");
    if (saved) ref.current.editor.loadDesign(JSON.parse(saved));
  };

  return (
    <>
      <button onClick={save}>Save</button>
      <button onClick={open}>Open</button>
      <EmailEditor ref={ref} token={import.meta.env.VITE_MAILKARO_KEY} style={{ height: "90vh" }} />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Merge tags

Each merge tag has a label, the value your email service fills in, and a sample:

<EmailEditor
  ref={ref}
  token={import.meta.env.VITE_MAILKARO_KEY}
  mergeTags={{
    first_name: { label: "First name", value: "{{first_name}}", sample: "Alex" },
    company: { label: "Company", value: "{{company}}", sample: "Acme" },
  }}
/>
Enter fullscreen mode Exit fullscreen mode

In a text block, your users pick Insert merge tag. The tag lands as a labelled pill, so they can see exactly where each field goes.

5. Render email-safe HTML on your server

Sending needs finished HTML, and you can make it on your own server from the saved design:

npm install email-craft-sdk express
Enter fullscreen mode Exit fullscreen mode
import express from "express";
import { renderDesign, renderMjml } from "email-craft-sdk";

const app = express();
app.use(express.json({ limit: "2mb" }));

app.post("/render", (req, res) => {
  const html = renderDesign(req.body.design, { fullDocument: true });
  const mjml = renderMjml(req.body.design);
  res.json({ html, mjml });
});

app.listen(3001, () => console.log("Server on http://localhost:3001"));
Enter fullscreen mode Exit fullscreen mode

There's no headless browser, no network call, and no key needed. It runs inside your backend.

6. Decide what each user can do

The plan unlocks when both keys work together: the publishable key in the browser, and the secret key on your server. Add one route:

import { Mailkaro } from "email-craft-sdk";

const mailkaro = Mailkaro.fromEnv(); // reads MAILKARO_SERVER_KEY (sk_live_...)

app.get("/mailkaro/embed-runtime", async (_req, res) => {
  res.json(await mailkaro.resolveRuntime());
});
Enter fullscreen mode Exit fullscreen mode

Point the editor at it with runtimeUrl:

<EmailEditor
  ref={ref}
  token={import.meta.env.VITE_MAILKARO_KEY}
  runtimeUrl="/mailkaro/embed-runtime"
  /* ...mergeTags, style */
/>
Enter fullscreen mode Exit fullscreen mode

In development, let Vite forward these routes to Node, so they run on the same origin as your app:

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: { "/mailkaro": "http://localhost:3001", "/render": "http://localhost:3001" },
  },
});
Enter fullscreen mode Exit fullscreen mode

Your server answers, and the secret key never leaves it.

7. Your own storage and your own AI

Add your S3 bucket and AI provider key to the server's .env, then three small routes:

import multer from "multer";

app.post("/mailkaro/ai", async (req, res) => {
  res.json(await mailkaro.ai(req.body.method, req.body.input));
});

const upload = multer();
app.post("/mailkaro/upload", upload.single("file"), async (req, res) => {
  res.json(await mailkaro.uploadAsset({
    buffer: req.file.buffer,
    filename: req.file.originalname,
    contentType: req.file.mimetype,
    scopeId: "demo", // your tenant or workspace id
  }));
});

app.get("/mailkaro/assets", async (_req, res) => {
  res.json(await mailkaro.listAssets({ scopeId: "demo" }));
});
Enter fullscreen mode Exit fullscreen mode

Now the image library has an Amazon S3 tab with your bucket's images, and new uploads go straight into it. The AI assistant runs on your own key and builds emails right on the canvas. The keys stay on the server, and the browser never sees them.

The whole loop

  • One token
  • One component
  • Two calls to save and reopen
  • One function to render on your server
  • One route that decides what each user can do

The free plan covers the editor itself. Pro unlocks storage, AI and more, and the first month is free with no card.

Next in the series: the same setup in Angular, Vue and plain JavaScript.

Top comments (0)