The React Email Builder Component You Own (Not an npm Package)
If you searched for a "React email builder component," you were probably hoping to run npm install, import an <EmailEditor/>, drop it in JSX, and move on. Here is the honest answer up front: there is no published npm package for BuilderJS and no <EmailEditor/> component to import. BuilderJS ships as a compiled, vanilla-JavaScript browser bundle — a single drag-and-drop editor engine for HTML email and web pages — that attaches a global window.Builder when its script loads. That sounds like a downgrade until you see what it actually gives you: instead of depending on someone else's React component (and their versioning, their props, their SaaS backend), you write a thin, ~30-line wrapper component of your own that mounts the engine and exposes it through a ref. You end up with a real, reusable <EmailBuilder/> in your codebase — one you own, self-host, and can shape to your app's exact needs. This page shows the self-owned wrapper pattern against the real BuilderJS API, explains why a global-script engine is the right call for a long-lived React app, and covers how you turn a finished design into Outlook-safe email HTML. BuilderJS is the editor you embed — it builds and exports the markup; it does not send email, host pages, or manage lists. You bring your own sending path; the component just lets people design.
Mount it
import { useEffect, useRef, useImperativeHandle, forwardRef } from 'react';
// builder.js + builder.css are loaded globally (e.g. in index.html),
// which attaches window.Builder. There is NO npm package to import.
// This file IS your "React email builder component" — a thin wrapper you own.
const EmailBuilder = forwardRef(function EmailBuilder({ initialDesign }, ref) {
const builderRef = useRef(null);
const mounted = useRef(false);
useEffect(() => {
if (mounted.current || !window.Builder) return; // React 18 Strict-Mode guard
mounted.current = true;
const builder = new window.Builder({
mainContainer: '#bjs-canvas', // REQUIRED — CSS selector string
widgetsContainer: '#bjs-widgets', // optional — CSS selector string
settingsContainer: '#bjs-settings', // optional — CSS selector string
});
builderRef.current = builder;
// load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) — 4 positional args
builder.load(
initialDesign ?? window.THEME_JSON,
window.THEME_TEMPLATES,
window.THEME_CONFIG_DATA,
window.MEDIA_URL
);
// No cleanup / destroy() — none exists; React discards the DOM on unmount.
}, []);
// Expose the instance so a parent can save/restore designs.
useImperativeHandle(ref, () => ({
getDesign: () => builderRef.current?.getData(), // JSON snapshot -> POST to your backend
getEmailHtml: () => builderRef.current?.getHtml(), // export markup for your ESP/SMTP
load: (...args) => builderRef.current?.load(...args), // 4-arg load()
}));
return (
<>
<div id="bjs-widgets" />
<div id="bjs-canvas" />
<div id="bjs-settings" />
</>
);
});
export default EmailBuilder;
// Usage:
// const editor = useRef(null);
// <EmailBuilder ref={editor} />
// const json = editor.current.getDesign(); // then POST json to your own backend
There is no npm <EmailEditor/> — you build a thin wrapper instead
The instinct with React is to reach for a package: npm install something, import { EmailEditor }, render it. BuilderJS deliberately does not work that way, and it is worth being clear so you don't waste time. There is no npm package, no module export, and no import { Builder } from anything — that line will fail to resolve because no such package exists. The engine is delivered as a self-hosted bundle you load with two tags: <link rel="stylesheet" href="/builderjs/builder.css"> and <script src="/builderjs/builder.js"></script>. Loading the script attaches a single global, window.Builder. Your job in React is not to find a component to install — it is to write one. The wrapper is small: render the three container divs the editor mounts into, instantiate new window.Builder(...) once inside useEffect after the DOM exists, and forward the instance through a ref so the parent can call getData() and load(). The result is a genuine React component named whatever you want — <EmailBuilder/>, <Editor/>, <Composer/> — that lives in your repo. The difference from an npm component is ownership: nothing breaks when a vendor publishes a major version, nothing phones home, and you can extend the wrapper (props, callbacks, toolbars) freely because you wrote it.
The self-owned wrapper: mount in useEffect, expose via ref
The pattern has three moving parts and one gotcha. First, render the container nodes in JSX up front — <div id="bjs-widgets">, <div id="bjs-canvas">, and <div id="bjs-settings"> — because the engine queries the DOM the moment it is constructed. Second, instantiate inside useEffect(() => {...}, []) so it runs once after the component's DOM is in the document, passing CSS selector strings (not refs, not DOM nodes) to the constructor: new window.Builder({ mainContainer: '#bjs-canvas', widgetsContainer: '#bjs-widgets', settingsContainer: '#bjs-settings' }). Only mainContainer is required; the two sidebars are optional and simply don't mount if you omit the selector. Third, store the instance in a ref (and optionally forward it with useImperativeHandle) so the parent component can drive saving and loading. The gotcha is React 18 Strict Mode: in development it intentionally double-invokes effects, and the Builder instance has no destroy(), teardown(), or dispose() method — so you cannot clean up between the two calls. Guard with a simple ref flag (if (mounted.current) return; mounted.current = true;) so the constructor runs exactly once. On a real unmount, React discards the container DOM node and the editor goes with it; there is genuinely nothing to dispose. Never call builder.destroy() — that method does not exist.
Persisting designs: getData() and the four-argument load()
A component is only useful if the parent can save and restore work, and BuilderJS keeps this intentionally plain. Expose the instance through your ref, then to save call const json = builder.getData() — a serializable JSON snapshot of the full design — and POST it to your own backend route. To restore, call builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl): four positional arguments, in that order — the saved theme/page JSON, the theme templates map, the theme config data (fonts, colors, sizes), and the base media URL for resolving relative asset paths (an optional callback may follow, but is rarely needed). The same load() call seeds a fresh editor with a starter template and rehydrates a previously saved design — just pass the snapshot you stored from getData() back as the first argument and you get an exact round-trip, not a best-effort re-parse. Because the format is plain JSON, the storage layer is entirely yours: a database column, object storage, a file — BuilderJS imposes no persistence backend, no autosave, and no server sync. This clean split is what lets the component stay focused: it hands you JSON and consumes JSON, and your app decides where designs live.
From the React component to real, Outlook-safe email HTML
"Email builder" implies the output actually survives an inbox, and this is where the component earns its place over a generic rich-text box. BuilderJS does not just emit pretty page markup and call it email. Its EmailExportPipeline takes the design your users build and produces client-safe email HTML: it converts modern flexbox and grid layouts into nested, role=presentation tables; adds MSO conditional comments and ghost tables for Windows Outlook (which still renders through Microsoft's Word engine); generates @media (max-width:600px) rules so columns stack on mobile; makes images fluid (max-width:100%; height:auto); rasterizes SVG to PNG for clients that don't support inline SVG; and runs a CSS inliner that pushes every safe declaration onto its element, because Gmail, Outlook, and Yahoo routinely strip <head> and external CSS. The library's 30+ shipped templates and 200+ samples are Litmus-tested across 22 email clients, with RTL and 20+ locales supported. In your React wrapper, you trigger this on export and hand the resulting HTML to whatever sends your mail. Which brings up the honest boundary: BuilderJS is the editor component, not a platform. It does not send email, manage subscriber lists, run deliverability, or host pages. The component lets people design and gives you portable JSON plus inbox-ready HTML; your backend (your ESP, your SMTP server) owns the send.
Why a global-script engine beats an npm component for a long-lived app
Depending on a third party's React email component feels convenient until you live with it. You inherit their release cadence, their breaking prop changes, their peer-dependency constraints on your React version, and — with most hosted SDKs — a recurring per-seat or per-month bill plus a backend you can't see. The self-hosted, global-script approach inverts all of that. Because you serve builder.js and builder.css from your own origin and wrap them in a component you wrote, the editor has no external runtime dependency on a vendor service: nothing phones home, your users' designs never leave your infrastructure, and your build pipeline doesn't pull a moving dependency. The engine is framework-agnostic by design — the same bundle drops into React, Vue, Svelte, Angular, or a plain page unchanged — so the only React-specific code is the thin wrapper you control. And the licensing matches the architecture: BuilderJS is sold under a one-time CodeCanyon license (item 27146783, Regular or Extended) with full source you self-host. No SaaS subscription, no monthly fee, no per-seat charge, no usage metering. For an email feature you expect to keep for years, owning a small wrapper around a source-licensed engine is the lower-risk, lower-cost path — you trade a five-minute npm install for a one-time setup that you never have to renegotiate.
Frequently asked questions
-
Is there an npm package or an <EmailEditor/> React component for BuilderJS?
No. BuilderJS is not on npm and has no module export or prebuilt React component. You self-host the compiled builder.js and builder.css (for example under public/builderjs/) and load them as a global browser script plus stylesheet; loading the script attaches window.Builder. Never write import { Builder } from anything — that will fail. Instead you write a thin wrapper component of your own that calls new window.Builder(...) inside useEffect, which becomes your reusable React email builder component.
-
How do I expose the editor so a parent component can save and load?
Store the Builder instance in a ref inside your wrapper, and forward it (for example with useImperativeHandle) so the parent can call methods on it. To save, the parent calls builder.getData() to get a JSON snapshot and POSTs it to your backend. To restore, it calls builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) with those four positional arguments in that exact order, passing the saved snapshot as the first one. Storage and transport are entirely yours.
-
Why does my editor mount twice in development, and how do I stop it?
That is React 18 Strict Mode intentionally double-invoking effects in dev to surface side-effect bugs. The Builder instance has no destroy(), teardown(), or dispose() method, so you cannot clean up between the two calls. Guard instantiation with a ref flag: const mounted = useRef(false); then inside useEffect, if (mounted.current) return; mounted.current = true; before constructing. This ensures exactly one Builder instance. On a real unmount React discards the container DOM and the editor with it, so no manual teardown is needed.
-
Does the component produce real email HTML or just page markup?
Real, client-safe email HTML. When you export for email, the EmailExportPipeline converts flexbox and grid layouts to nested role=presentation tables, adds MSO conditional comments and ghost tables for Windows Outlook, generates @media rules for mobile stacking, makes images fluid, rasterizes SVG to PNG, and inlines CSS so Gmail, Outlook, and Yahoo don't strip your styles. The 30+ shipped templates and 200+ samples are Litmus-tested across 22 clients. The same design can also export as clean page HTML for the browser.
-
Does the React component send the email or manage subscriber lists?
No. BuilderJS is the embeddable editor component only — it builds the design and exports JSON plus Outlook-safe email HTML. It does not send email, run deliverability, manage lists or CRM, or host pages. You connect the exported HTML to your own sending path: your ESP, SMTP server, or backend. The component owns the markup; your application owns the send and the storage. This separation is deliberate and is what keeps you in full control of delivery.
-
Can I render the wrapper with server-side rendering, like in Next.js?
No. BuilderJS is a client-side DOM-mount bundle that must run in the browser after the DOM exists, so it is never server-rendered. In an SSR framework like Next.js, mark the wrapper as a client component and load the editor only on the client (for example a dynamic import with SSR disabled), then instantiate inside useEffect. The server can render the three container divs; the engine attaches to them on the client.