Drag & drop
Users build layouts by moving blocks onto the canvas, in the order they want them.
PageKit — the self-hosted GrapesJS site builder, sold as source. Get early access
Give your users a visual way to create responsive email templates — reusable blocks, MJML, custom components and production-ready HTML export, all inside your own product.
The loop your users live in
26k+
GitHub stars
100+
Plugins on GJS.Market
10 years
Active development
BSD-3-Clause
Core license
Open a template off the shelf, change it, then save it as a new one. The shelf grows by one. That loop — not the individual designs — is what an email template builder gives your users.
Template library
Add a block
Your logo
Welcome aboard, {{ user.first_name }}
Here is how to get your first project running.
Open your dashboard
Unsubscribe · Manage preferences
Merge tags stay untouched — your renderer resolves them per recipient at send time.
The demo above is a model of the loop. This is GrapesJS itself, running the official MJML preset — loaded only when you ask for it, so it costs this page nothing until then.
Open in a new tabThe MJML editor demo published by the GrapesJS project itself. Add a section, edit the copy, then look at the generated markup.
Loads in an iframe from the demo's own site.
On GJS.Market
MJML editor is available as a product
An email template builder lets people create and change email layouts visually instead of hand-writing HTML email markup. A template *library* ships finished designs; a template *builder* ships the ability to make them — and to keep making them after you have shipped.
Write the HTML
tables, inline styles, conditional comments
Test across clients
you cannot see the result while writing
Hand it to marketing
as a file, or pasted into a tool
Every change comes back to you
a developer ticket per tweak
One template, one owner, one bottleneck.
Open a starting point
from the shelf, or blank
Edit visually
the result is on screen the whole time
Save it as a template
a row your customer owns
Reuse and version it
no ticket, no developer
Many templates, owned by the people who send them.
Templates give your users a starting point. A builder gives them control.
A library is a shelf with a ceiling: it covers what it was built to cover, and requests above that line have nowhere to go. A builder is a loop — and the shelf grows every time someone goes round it.
"We need a clean welcome email by Friday."
Template library
Solved. This is exactly what a good library is for — pick one, change the words, send.
Template builder
Also solved, from the same starting points. A builder ships with a library inside it.
"Every tenant needs their own legal footer."
Template library
One shared footer, or a fork of every template per tenant.
Template builder
One footer block, saved once per tenant and reused across their templates.
"Put the customer's actual order in the receipt."
Template library
Static placeholder text. The data has to be spliced in downstream.
Template builder
A component bound to your order model, editable as a block and resolved at send time.
"Restore the version we sent last Tuesday."
Template library
Whatever the file was overwritten with is what you have.
Template builder
A version row. Restoring is a query, not an archaeology project.
"Let our customers build their own, without emailing us."
Template library
Not what a library does — it has no authoring surface.
Template builder
The whole point. Your users create and keep templates you never see.
Ship the library. Then ship the thing that makes more of them.
The editing surface your users get, and the parts of it you control.
Users build layouts by moving blocks onto the canvas, in the order they want them.
Text, images and links are edited in place, with the rendered result on screen the whole time.
Sections your users save once and drop into any template afterwards.
Preview and adjust the layout at the widths that matter for email, while editing.
Author against mjml where you want responsive markup generated for you rather than hand-maintained.
Produce the email HTML your delivery workflow already expects.
Manage the images a template references, in whatever store you already use.
Add editable components that mirror your own product's data and workflows.
A full history stack, so experimenting with someone else's template is safe.
Give users a library of sections to assemble from, instead of making them start every email from an empty canvas. Each one is a block definition you register once.
Logo, preheader text and the navigation row, if you use one.
The opening statement and the first call to action.
A row or grid of items, bound to your catalogue where you want it live.
Short icon-and-text rows for release notes and onboarding.
Plan comparison laid out to survive a table-based renderer.
A quote, an attribution and an optional avatar.
A bulletproof button that renders without background images.
Address, preferences link and the unsubscribe link you are legally required to include.
Standard blocks cover layout. What makes the builder yours is the components that know about your data — an order, a subscription, a booking, a recommendation.
Standard
The primitives every email needs. They come with the editor.
Composed
Your standard blocks, assembled once and registered for reuse.
Bound
Components with traits your users set and merge tags your renderer resolves per recipient.
The editor renders a placeholder while the template is being written. Your backend swaps in the real values at send time.
// A component bound to your own model. The editor renders a placeholder; your
// backend swaps in the real order at send time, per recipient.
editor.DomComponents.addType('order-summary', {
model: {
defaults: {
droppable: false,
// Traits are the fields the person editing the template can set.
traits: [
{ name: 'orderId', label: 'Order', type: 'text' },
{ name: 'showTax', label: 'Show tax line', type: 'checkbox' },
],
// Merge tags survive export untouched and are resolved by your renderer.
components: '<mj-text>{{ order.total }}</mj-text>',
},
},
});In a template system, templates differ by who owns them and who may change them — not only by what they say. These are the classes worth modelling from day one.
Ownership classes
These are ownership classes in a template system, not products for sale. GJS.Market lists editor plugins and presets; it does not sell email templates.
Preview the layout at the widths email actually renders at, while you are still editing it. 600px is the long-standing convention for the body column — a convention, not a specification.
What users can change per width
Two columns on a desktop client, stacked on a phone — decided in the template, not by the recipient's client guessing.
Email clients implement HTML and CSS differently, and always have. A visual builder does not remove that — it standardises the authoring step so the same known-good markup comes out every time.
| Client | What it does differently | What the builder does about it |
|---|---|---|
| Gmail | Strips <style> blocks in some contexts, including the Gmail mobile apps with non-Gmail accounts. | Emit inlined styles rather than relying on a stylesheet surviving the trip. |
| Outlook (Windows) | Renders with the Word engine: no flexbox, no grid, and border-radius on a table cell is dropped. | Generate table-based layout and bulletproof buttons instead of CSS layout. |
| Apple Mail | The most standards-complete of the four; also the most likely to expose a layout that only worked by accident elsewhere. | Preview at each width while editing, so the layout is deliberate rather than incidental. |
| Yahoo Mail | Historically inconsistent with media queries; current behaviour is Not publicly documented. | Prefer layouts that degrade to a single readable column without a media query. |
Constraints as verified on 2026-08-28. Behaviour changes; check caniemail.com for the current state, and test production emails against the clients your own audience actually uses.
mjml exists to make responsive email markup something you generate rather than hand-maintain. GrapesJS can be extended with grapesjs-mjml to put a visual editing surface on top of it.
MJML is compact enough to review in a pull request, which raw email HTML is not.
The compiler emits the table scaffolding and media queries, so nobody hand-writes them.
Keep the MJML alongside the project JSON, so a template stays editable as source.
The question that decides whether you have a builder or a viewer: what rows exist after a user clicks Save? Three tables cover it.
templates
One row per template, scoped to a tenant.
template_versions
Append-only. Restoring last Tuesday's email is a query.
blocks
Saved once, dropped into any template afterwards.
One version row holds all three
JSON
Project data
What you load back into the editor. Without it a template is read-only forever.
MJML
Editable source
Diffable, reviewable, and re-compilable when the compiler improves.
HTML
What you send
Rendered and inlined, handed to your provider unchanged.
Store all three. Each answers a different question, and reconstructing any one of them from the others is lossy.
How the three formats relateThe editor is a client. It asks your API for a template and hands one back; it never learns who your tenants are, and it never holds the only copy of anything.
Where a template goes
// Storage scoped to the tenant AND the template. The editor never learns who
// the tenant is — your API does, from the session, and refuses everything else.
editor.Storage.add('remote', {
async load({ templateId }) {
const res = await fetch(`/api/templates/${templateId}/project`);
return res.json();
},
async store(project, { templateId }) {
await fetch(`/api/templates/${templateId}/project`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(project),
});
},
});Integrate the editor with the backend you already run, rather than moving your email workflow onto someone else's platform. GJS.Market sells editor plugins — it does not host your templates and does not send your email.
The moment a second customer saves a template, ownership stops being a detail. Three tiers cover almost every product.
Yours. Visible to every tenant, editable by none of them — the starting points you maintain.
Saved by a customer, visible to their whole team, invisible to everyone else.
Work in progress. Private until someone promotes it to a tenant template.
The same template system, pointed at different jobs.
Let customers build campaigns visually and keep every send as a versioned template.
Embedding the editorReusable outreach templates a whole team shares, without a designer in the loop.
Drag-and-drop email builderTemplates for automated journeys, with components bound to the event that triggered them.
React email builderA no-code design workflow for writers who should never see a table layout.
Headless CMS editorEmail creation inside the product people already have open, not in a separate tool.
White-label page builderControlled editing: brand-locked regions, editable copy, merge tags that cannot be broken.
Embeddable page builderThe editing canvas is the visible part. A template system is the rest of it — and the rest is where the time goes.
What a template system is made of
From scratch
All of it
With GrapesJS
The editing layer, already built
GrapesJS gives you the editing layer. The template system on top of it — ownership, versioning, tenancy — is yours, and should be: it is the part that matches your product.
Real listings from the catalogue, grouped by the job they do inside a template system. Names and prices come straight from the marketplace, so nothing here can drift from what is actually for sale.
Saving, listing and reopening templates — the subject of this page.
Browse this categoryFour combinations that already work together, chosen by whose template library you are building.
For customers who each keep their own library
Embedding in a SaaSFor one team producing a lot of campaigns
For templates that live beside your other content
Headless CMS editorFor an editor mounted inside a React app
Prices are read live from the catalogue at build time.
GrapesJS is the editing layer and stops at the HTML. Everything after that is your backend and the provider you already pay — there is no sending built into the editor, and none implied here.
Stage 01
The send references a specific version, so what went out stays reproducible after the template moves on.
template_versions.idStage 02
Your renderer resolves the merge tags per recipient. The editor never sees customer data.
{{ order.total }}Stage 03
Table-based and inlined, which is what every provider expects to be handed.
text/htmlStage 04
Whichever ESP you already use. Swapping it changes nothing about the template system.
POST /messagesStage 05
Opens, clicks and bounces attach to the version that produced them, which is what makes them comparable.
webhookThe editor core plus the official MJML plugin. Both are BSD-3-Clause.
npm install grapesjs grapesjs-mjmlimport grapesjs from 'grapesjs';
import grapesjsMjml from 'grapesjs-mjml';
import 'grapesjs/dist/css/grapes.min.css';
// One editor instance per template being edited. The template id — not a page
// id — is what every load and store call is keyed by.
const editor = grapesjs.init({
container: '#template-editor',
plugins: [grapesjsMjml],
pluginsOpts: {
[grapesjsMjml]: {
imagePlaceholderSrc: 'https://cdn.your-app.com/email/placeholder.png',
},
},
storageManager: {
type: 'remote',
autosave: true,
stepsBeforeSave: 10,
},
});Saving is where a template builder stops being an editor. "Save as template" creates a row your customer owns; saving again appends a version rather than overwriting one.
// "Save as new template" — a row your customer owns from now on.
async function saveAsTemplate(name) {
const res = await fetch('/api/templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
project: editor.getProjectData(), // re-openable in the editor
}),
});
return res.json(); // { id, name, version: 1 }
}
// Editing an existing template appends a version rather than overwriting it,
// so "restore the version we sent last Tuesday" stays a query, not a restore.
async function saveVersion(templateId) {
await fetch(`/api/templates/${templateId}/versions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project: editor.getProjectData() }),
});
}Framework specifics live on their own pages — this page stays framework-neutral on purpose.
Teams that need email creation to happen inside their product rather than beside it.
Embed visual email editing in your application, so customers stop exporting to a separate tool.
Give customers a campaign builder whose templates you can version, audit and support.
Start from an extensible editing foundation instead of writing a canvas and a history stack.
Build a reusable email system per client, handed over as blocks and locked templates.
Add email creation without owning an editor core and its maintenance forever.
Let users produce on-brand email without writing markup or filing a ticket.
This page is about the template system. These cover the neighbouring questions.
MJML in, inlined table-based HTML out, handed to a sending provider. The format and delivery argument in full.
GrapesJS email builderThe editing experience from the marketer's side, rather than the data model underneath it.
Drag-and-drop email builderMounting and controlling the editor from a React application.
React email builderThe same embedding question for pages rather than email.
SaaS page builderStart with GrapesJS, shape the editing experience around your product, and extend it with the plugins your template system needs.
Install the core, mount the editor and wire storage to your own API.
Read the quick startPresets, blocks, storage and export — real listings with live prices.
Browse email pluginsTell us what your template system has to do, and we will scope the build.
Start a briefRelated reading