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
-
Does BuilderJS fill in the ticket ID, agent name, and status, or send the reply?
No. BuilderJS is the embeddable editor only. It treats your merge tokens — {{ticket.id}}, {{agent.name}}, {{ticket.status}} or whatever syntax you already use — as literal text in the design and carries them unchanged through to the exported HTML. Your help-desk platform does the actual substitution at send time and routes the message through your ESP or SMTP, exactly as it does for plaintext canned replies today. The editor owns the branded layout; your platform owns the ticket data and the send.
-
How does this differ from a plaintext canned-reply editor?
Canned replies are raw text your platform substitutes tokens into. BuilderJS is for the next step up: designed, branded HTML templates — agent replies with your header and footer, ticket-status notifications, escalation alerts, CSAT surveys — where layout, color, and an Outlook-safe render matter. The same merge tokens still work because they are just text in the design; what you gain is a drag-and-drop canvas and an export pipeline that makes the result survive real inboxes. If all you need is plaintext, you don't need a visual builder; this is scoped to platforms doing branded templates.
-
How do I store one template per language for global support?
Store one JSON design per locale. Call builder.getData() to get a lossless snapshot and POST it to your backend — a row per locale (en, de, ja, fr) keyed to the template family — so your platform's existing locale resolution picks the right variant at send time. To re-open any variant, call builder.load(themeJson, themeTemplatesJson, themeConfigData, mediaUrl) with those four positional arguments, passing the saved snapshot first. Because designs are plain JSON, they version like config: commit them to git, diff wording changes in review, and migrate a property across all variants with a script.
-
Will a ticket reply or CSAT survey render correctly in Outlook and Gmail?
That is what the EmailExportPipeline is for. On export it converts flexbox and grid to nested role=presentation tables, adds MSO conditional comments and ghost tables for Windows Outlook, generates @media mobile-stacking so columns collapse on a phone, makes images fluid, rasterizes SVG to PNG, and inlines CSS — because Gmail, Outlook.com, and Yahoo strip head and external styles. The shipped templates and samples are Litmus-tested across 22 email clients. BuilderJS produces the inbox-safe markup; your platform hands it to your sending path.
-
Can I embed it in a multi-tenant support platform, and is there a per-seat fee?
Yes. BuilderJS is multi-tenant capable via closure-captured configuration, so each tenant's editor instance is isolated, and there is no per-seat charge, usage meter, or revenue share. The honest caveat is that your platform owns tenant isolation, seats, and billing — which it already does. It is a one-time CodeCanyon license (Regular $52 / Extended $99); if your end users are charged, choose the Extended tier and review the license terms for your distribution model before launch.