BuilderJS

An Embeddable, Outlook-Safe Template Editor for the Email Your Help Desk Sends Most

A garbled ticket-reply or a CSAT survey that collapses in Outlook is not a cosmetic problem — it is itself a support failure. If your help desk or support platform lets teams design branded agent-reply templates, ticket-status notifications, escalation alerts, and satisfaction surveys, the editor your customers use to build those templates is part of your product surface, and so is the markup it emits. BuilderJS is built to be exactly that editor: a framework-agnostic, vanilla-JavaScript drag-and-drop builder for HTML email and web/landing pages that you self-host and mount inside your own application — React, Vue, Svelte, Angular, Next.js, Laravel, or a plain server-rendered admin page. It is the editor surface only, and that boundary is the whole point. BuilderJS builds and exports the template markup; your platform keeps owning everything around it — the ticket data, the agent and queue model, contact records, the merge-field substitution, the SLA timers, scheduling, sending, and deliverability. This page is scoped to platforms doing designed, branded templates, not plaintext canned replies. It explains where the editor fits in a ticket lifecycle, how it keeps merge tokens like ticket ID, agent name, and status intact through an Outlook-safe export, how to store a template per locale as plain JSON, and the exact mount-and-persist pattern your team can ship. Designs round-trip losslessly through getData() and load(), so your platform owns persistence end to end, there is no per-seat SDK bill that scales with your customer base, and nothing phones home.

Mount it

import { useEffect, useRef } from 'react';
  
  // Self-host builder.js + builder.css and load them globally (e.g. in index.html),
  // which attaches window.Builder. There is NO npm package and NO import.
  export default function TicketTemplateEditor({ template, onSaved }) {
    const mounted = useRef(false);
    const instance = useRef(null);
  
    useEffect(() => {
      if (mounted.current || !window.Builder) return; // React 18 Strict-Mode guard
      mounted.current = true;
  
      instance.current = new window.Builder({
        mainContainer: '#bjs-canvas',       // REQUIRED — CSS selector string
        widgetsContainer: '#bjs-widgets',   // optional — drop to slim the chrome
        settingsContainer: '#bjs-settings', // optional
      });
  
      // Restore a saved reply/CSAT template for one locale. load() takes 4 positional
      // args in this order: load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl).
      // template.json is the admin's saved snapshot from a prior getData().
      // Merge tokens like {{ticket.id}} are stored as plain text — BuilderJS does NOT
      // substitute them; your help-desk platform fills them in at send time.
      instance.current.load(
        template.json,            // saved design snapshot (per-locale variant)
        template.themeTemplates,  // theme templates JSON
        template.themeConfig,     // theme config data
        template.mediaUrl         // media base URL for asset resolution
      );
      // No destroy()/dispose() exists — React discards the DOM on unmount.
    }, []);
  
    // Persist: capture lossless JSON, POST to YOUR backend keyed by template + locale.
    // BuilderJS never sends email and never stores designs for you.
    const handleSave = () => onSaved(instance.current.getData());
  
    return (
      <>
        <div id="bjs-widgets" />
        <div id="bjs-canvas" />
        <div id="bjs-settings" />
        <button onClick={handleSave}>Save template</button>
      </>
    );
  }

Where the editor fits in the ticket lifecycle — and the honest line

Support email is high-volume and high-stakes: a customer who opened a ticket is already mildly unhappy, and a reply that renders as a wall of broken tables in Outlook makes it worse. The natural place to design these templates is inside your own admin, where the people who configure agent macros, status notifications, and CSAT surveys already work — not in a separate design tool they copy markup out of and hope survives the inbox. Embedding BuilderJS in your template-management screen means an admin clicks 'New reply template' and a drag-and-drop canvas opens inside your dashboard, under your brand, with no third-party login in the middle. The line that keeps this honest: BuilderJS is the editor component, not a help-desk platform. It does not create or route tickets, assign agents, track SLAs, manage queues or contacts, substitute merge values, send email, or measure CSAT scores — those are precisely the things your platform already does and should keep owning. BuilderJS produces the branded template; your platform fills in the ticket fields at send time, picks the recipient, and routes it through your ESP or SMTP. The contract is tidy: the editor builds the design, your platform owns the ticket and the send. You are adding a best-in-class design surface to support email, not bolting on a second product that competes with your own.

Merge tokens stay intact: ticket ID, agent, status, queue

Branded support email is personalized email — a reply that doesn't say the ticket number, the agent's name, and the current status is generic and unhelpful. The important thing to be precise about: BuilderJS does not run a merge engine, and pretending otherwise would be dishonest. What it does is treat your placeholder tokens as ordinary text in the design. An admin types {{ticket.id}}, {{agent.name}}, {{ticket.status}}, or whatever token syntax your platform already uses, directly into a heading, a paragraph, or a button label, and the editor stores that literal text in the JSON and carries it through to the exported HTML unchanged. Your platform does the actual substitution at send time, exactly as it does today for plaintext canned replies — the only thing that changes is the surrounding template is now a designed, branded layout instead of raw text. Because tokens are just characters, they survive everything the export does to the markup: they don't get rewritten by the CSS inliner, mangled by the flexbox-to-table conversion, or stripped by the Outlook fallbacks. That means the merge syntax your support platform already supports keeps working with zero changes on your side; BuilderJS simply gives agents a visual canvas to place those tokens inside a layout that looks like your brand.

Outlook-safe export — because a broken reply is a support failure

Ticket replies and CSAT surveys go to real inboxes, and that means Windows Outlook, Gmail, Yahoo, and Apple Mail, each with its own quirks. BuilderJS handles this through its EmailExportPipeline: 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 mail through Microsoft's Word engine), generates @media (max-width:600px) rules so columns stack cleanly on a phone — where a large share of support email is read — makes images fluid (max-width:100%; height:auto), rasterizes SVG to PNG for clients that won't render vectors, and runs a CSS inliner that pushes every safe declaration onto its element, because Gmail, Outlook.com, and Yahoo routinely strip head and external CSS. The library ships 30+ templates and 200+ samples that were Litmus-tested across 22 email clients, so what an agent exports is built for inboxes that actually exist rather than a clean-room browser. And because many support platforms also need a hosted status page, a help-center article layout, or a survey landing page, the same editor produces standalone modern-CSS page HTML from the same design — one embedded tool covers both your branded email templates and your page needs. To keep the handoff precise: BuilderJS builds and exports the HTML; your platform brings the sending for email and the hosting for pages.

Per-locale templates that version like config — plain JSON

Global support means the same ticket-reply template often needs to exist in several languages, and the clean way to manage that is one design per locale rather than one design with runtime branching. BuilderJS designs are plain JSON: call builder.getData() and you get a serializable snapshot of the full design tree — every element, its property values, and the nesting between them. You POST that JSON to your own backend and store it however your platform already stores records — for a localized template, a row per locale (en, de, ja, fr) keyed to the template family, so your platform's existing locale-resolution logic picks the right one at send time exactly as it does for canned-reply text today. To re-open any variant for editing, call builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) — exactly four positional arguments, in that order — passing the saved snapshot back as the first one, and the editor rebuilds the precise state losslessly. Because the data is just JSON, templates version like the rest of your config: you can commit them to git, diff a wording change in review, and migrate a property across thousands of saved designs with a script. There is no proprietary store you get evicted from, no usage meter counting templates, and no export wizard gating clean code behind a paid tier. (Note: BuilderJS's own i18n covers the editor chrome — its UI ships in 20+ locales with RTL — which is separate from the recipient-facing content; the per-locale content lives in the JSON variants you store.) The package also includes reference PHP backends (MySQL / SQLite / S3 for save, upload, and auth) as working examples you copy, own, and can port to Node, Go, or Python.

Mount it in your admin, own the source outright

BuilderJS is delivered as a compiled browser bundle, not an npm module — loading builder.js attaches a global window.Builder, so there is no import { Builder } from anything and nothing to install from a registry. You self-host builder.js and builder.css from your own origin and add them with a stylesheet link and a script tag. The constructor takes CSS selector strings (not DOM nodes, not refs): mainContainer is required and resolves the canvas, while widgetsContainer and settingsContainer are optional sidebars — a selector that resolves to nothing simply leaves that panel unmounted, handy when your support admin is tight on horizontal space and you want leaner chrome. It is a client-side DOM-mount editor and is never server-rendered, so mount it after the DOM exists: inside useEffect in React, onMounted in Vue, or ngAfterViewInit in Angular; in Next.js use a dynamic import with ssr disabled. There is no destroy(), teardown(), or dispose() method — on a real unmount your framework discards the container DOM node and the editor goes with it, so the only thing to guard is constructing it exactly once (a ref flag survives React 18 Strict Mode's intentional double-invoke in development). On the commercial side, embeddable builder SDKs sold as SaaS bill you every month for as long as your platform exists, and the cost tends to scale with your success. BuilderJS is the opposite model: a one-time CodeCanyon license (item 27146783, Regular $52 / Extended $99), full source delivered, self-hosted, with no monthly fee, no per-seat charge, no usage metering, and no phone-home. The honest boundary: the code is yours to keep and run perpetually, but author support and version updates are time-boxed (roughly 6–12 months under CodeCanyon's standard window), after which you self-maintain the source you own — never 'lifetime.' If your support platform charges its end users, the Extended tier is the one to choose. Try the live editor and the getData()/load() round-trip at builder.emotsy.com before you commit.

Frequently asked questions

Explore