BuilderJS

An HTML Email Editor SDK You Own and Self-Host

If you are searching for an "HTML email editor SDK," you are almost certainly a developer who needs to drop a visual email designer into your own product — a SaaS dashboard, an internal tool, an agency platform — without building a drag-and-drop editor from scratch and without renting one forever. BuilderJS is that SDK, with one important honesty up front: it is the editor surface, not a sending platform. It is around a 140 KB gzipped vanilla-JavaScript engine you mount inside any DOM node to give your users a visual, drag-and-drop builder for both HTML email and web/landing pages. It produces the markup; your app owns persistence, authentication, asset upload, and delivery. BuilderJS does not send email, manage lists, handle deliverability, or run analytics — your backend or ESP does that, and BuilderJS ships reference handlers to make wiring those parts straightforward. What makes it an SDK rather than a SaaS widget is the ownership model: you buy a one-time CodeCanyon source license (item 27146783, Regular $52 / Extended $99), self-host the bundle from your own origin, and embed it in your own screens with no monthly fee, no per-seat pricing, no usage metering, and no phone-home. It is framework-agnostic — the same bundle runs in React, Vue, Svelte, Angular, Laravel, or plain HTML — and JSON-first, so getData() and load() give you a lossless round-trip of every design as portable data you control. This page covers what the SDK surface actually looks like, how the email export pipeline produces Outlook-safe HTML, how persistence works, and where BuilderJS fits honestly against hosted email-editor SDKs.

Mount it

// BuilderJS has no npm package and no React component. Wrap the global
  // window.Builder engine in your OWN thin component (mount in useEffect,
  // expose the instance via ref). builder.js + builder.css are self-hosted
  // and loaded globally (e.g. in index.html), which attaches window.Builder.
  import { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
  
  const EmailEditor = forwardRef(function EmailEditor(_, 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 block palette
        settingsContainer: '#bjs-settings', // optional properties panel
        upload: { url: '/api/upload', maxBytes: 5_000_000 }, // your endpoint
      });
      builderRef.current = builder;
  
      // load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) — 4 args
      builder.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG, MEDIA_URL);
  
      // Events ride the bus (there is NO on:{} constructor option):
      builder.events.on(window.Builder.EVENTS.DOCUMENT_CHANGED, () => {
        /* mark dirty / queue autosave with getData() */
      });
      // No destroy()/dispose() exists — React discards the DOM on unmount.
    }, []);
  
    // Expose a self-owned API to the parent: persist via getData(), not a vendor.
    useImperativeHandle(ref, () => ({
      getData: () => builderRef.current?.getData(),  // JSON snapshot -> your DB
      getHtml: () => builderRef.current?.getHtml(),  // rendered email HTML
    }));
  
    return (
      <>
        <div id="bjs-widgets" />
        <div id="bjs-canvas" />
        <div id="bjs-settings" />
      </>
    );
  });
  
  // Parent: const ref = useRef(); then POST ref.current.getData() to your backend.
  export default EmailEditor;

What the SDK surface actually is — a global window.Builder, not an npm package

BuilderJS is delivered as a compiled browser bundle, not an ES module — so being clear about the integration shape matters before you write a line of code. You load two assets that you self-host: the stylesheet (/dist/builder.css) and the script (/dist/builder.js). Loading the script attaches a single global constructor, window.Builder. There is no published npm package and no named module export, so you never write import { Builder } from 'builderjs' — that line will fail because no such package exists. Instead you reference the global directly: new Builder({ mainContainer, widgetsContainer, settingsContainer }). The constructor takes CSS selector strings (passed internally to querySelector), not DOM nodes or refs. mainContainer is required and resolves the editing canvas; widgetsContainer (the block palette) and settingsContainer (the properties panel) are optional, and any sub-panel whose selector resolves to nothing simply stays unmounted. The full options object also accepts saveUrl (a string endpoint), an upload config ({ url, maxBytes }), an asset-picker callback (onBrowse), and loadAssets ('auto' or false). Note that runtime events are NOT a constructor option — there is no on:{} event map. Instead the live instance exposes an event bus you subscribe to after construction: builder.events.on(Builder.EVENTS.DOCUMENT_CHANGED, …) and Builder.EVENTS.DOCUMENT_SAVED. This thin, selector-driven surface is exactly what keeps the same engine framework-agnostic — it has no opinion about your bundler, your component tree, or your build step.

The instance API: getHtml(), getData(), load(), save()

Once mounted, the Builder instance gives you a small, deliberate API. builder.getData() returns a serializable JSON snapshot of the current design — this is your source of truth and what you persist. builder.getHtml() returns the rendered HTML for the design. builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) takes those four positional arguments, in that order, to hydrate the editor with a saved design or a starter template; feeding a previously saved getData() snapshot back through load() reconstructs the exact design. builder.save() POSTs html and JSON.stringify(data) as application/x-www-form-urlencoded to the saveUrl you configured. Two things to internalize: saving is host-driven — there is no save:click event — so if you need slug, CSRF, or custom routing, skip saveUrl and hand-roll a fetch around getData() and getHtml() yourself; for autosave and dirty-state, subscribe to the event bus (builder.events.on(Builder.EVENTS.DOCUMENT_CHANGED, …) fires, debounced, on every mutation). And there is no destroy(), teardown(), or dispose() method on the instance — on a real unmount your framework discards the container DOM node and the editor goes with it, so there is genuinely nothing to clean up. In React 18 Strict Mode, where effects double-invoke in development, guard instantiation with a simple ref flag so the constructor runs exactly once.

The email export pipeline — turning a modern design into Outlook-safe HTML

The hardest part of any HTML email editor is not the drag-and-drop — it is making the output survive Gmail, Outlook, and Yahoo, which strip head and external CSS and render with a 1990s rendering engine in desktop Outlook. BuilderJS handles this in its EmailExportPipeline. It converts modern flexbox and grid layouts into nested tables with role="presentation", adds MSO conditional comments and ghost tables for Windows Outlook, generates @media (max-width:600px) rules for mobile stacking, makes images fluid (max-width:100%; height:auto), rasterizes SVG to PNG for clients that do not render vector graphics, and runs a CSS inliner that pushes every safe declaration directly onto its element — because inline styles survive inbox sanitizing far more reliably than a head <style> block. A small head <style> is still kept for things that cannot be inlined (media queries, :hover), as progressive enhancement on top of the inlined base. The library ships 30+ templates and 200+ samples that are Litmus-tested across 22 email clients, so the pipeline is validated against the same fussy inboxes you are targeting. For browser-bound landing pages, the same design exports as clean modern-CSS page HTML instead — dual output from one editor, with the right CSS strategy applied automatically per destination.

Persistence and uploads are yours — the SDK stays a focused editor

BuilderJS is JSON-first and storage-agnostic by design. To save, call getData() and POST the JSON to your own backend route; to restore, pass it back through load(). The format is plain JSON, so you keep full control of the storage layer — a database column, object storage, or a flat file, whatever fits your stack. Asset upload follows the same principle: you configure upload:{ url, maxBytes } and BuilderJS POSTs uploaded media to your endpoint, but your backend owns where files land and how they are served (the mediaUrl you pass to load() is the base URL the editor uses to resolve them). maxBytes is a client-side hint, not enforcement — validate size, MIME type, and dimensions on the server too. To make this fast to wire, BuilderJS ships reference PHP backends for save, upload, and auth across MySQL, SQLite, and S3, and they are straightforward to port to Node, Go, or Python. This clean split is the whole point of an SDK rather than a platform: BuilderJS owns the editing experience and the markup it produces, and you own — and keep — everything around it. There is no vendor database holding your users' designs hostage and no paid tier gating clean export.

How it compares to a hosted email-editor SDK

Hosted embeddable editors — Unlayer, Beefree SDK, Stripo and the like — are genuine SaaS products: you embed their editor, but the engine runs against their service on a recurring subscription, and your customers' designs typically live in their infrastructure. BuilderJS sits in a different category. You buy the source once under a CodeCanyon license (Regular $52 / Extended $99), serve builder.js and builder.css from your own origin, and the editor has no external runtime dependency on a vendor service — which keeps your app fast, private, and fully under your control. The honest trade-offs: a hosted SDK gives you automatic updates and a managed backend out of the box, while BuilderJS asks you to self-host and bring (or build) your own save, upload, and delivery layer — the reference handlers shorten that work but do not remove it. Author support through CodeCanyon is time-boxed (typically 6–12 months); after that you keep and maintain the source yourself. And BuilderJS is the editor only: it never sends, hosts, or schedules email. If you want to embed a drag-and-drop email designer your customers use inside your product, own the code outright, and avoid per-seat-or-per-month fees that scale with your growth, the one-time-license model is the wedge. If you want a managed service to also handle delivery, a sending platform is what you want — and BuilderJS can be the editor that feeds it.

Frequently asked questions

Explore