npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

Readme

React Email Editor & React Email Template Builder, react-email-craft

npm version downloads license

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 ref methods match react-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:

Open in StackBlitz Edit in CodeSandbox

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-craft

Quick 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 JSON

3. 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 token is missing, wrong, or expired. A missing or invalid token safely falls back to the free feature set.
  • ref.current.editor is undefined → call editor methods after onReady fires (the editor needs a moment to initialize).
  • Images don't stick → make sure onImageUpload returns { url } with a public URL.

License

MIT