<bmx-email-designer>
Builds marketing and transactional emails by dragging blocks - headings, text, buttons, images, dividers, spacers and lines of links - into rows of one to three columns, and sends back HTML that email clients actually draw: tables, inline styles, columns that stack on a phone, Outlook for Windows looked after, preview text, and a plain-text part to go with it.
7 properties · 1 events · 10 methods · 4 parts
Example
Click a block to change it, drag blocks and rows from the Add list, and switch to Preview to see the HTML a mail client receives. Problems lists what to fix before sending.
Show markup
<bmx-email-designer id="ex-ed" label="Autumn newsletter"></bmx-email-designer>
<p class="note" id="ex-ed-out" role="status" style="margin: 12px 0 0">Click a block to change it, drag blocks and rows from the Add list, and switch to Preview to see the HTML a mail client receives. Problems lists what to fix before sending.</p>
<script type="module">
await customElements.whenDefined('bmx-email-designer');
const designer = document.getElementById('ex-ed');
const out = document.getElementById('ex-ed-out');
// A banner drawn here, so the example needs no image files. A real page
// gives image addresses, or stores uploads through `imageHandler`.
const banner = (() => {
const c = document.createElement('canvas');
c.width = 1200;
c.height = 440;
const g = c.getContext('2d');
const sky = g.createLinearGradient(0, 0, 1200, 440);
sky.addColorStop(0, '#7a3b16');
sky.addColorStop(1, '#d98a3d');
g.fillStyle = sky;
g.fillRect(0, 0, 1200, 440);
g.fillStyle = 'rgba(255, 240, 220, 0.18)';
for (let i = 0; i < 9; i += 1) {
g.beginPath();
g.ellipse(150 + i * 120, 330 - (i % 3) * 40, 46, 30, 0.6, 0, Math.PI * 2);
g.fill();
}
g.fillStyle = '#fff7ec';
g.font = 'bold 76px Georgia, serif';
g.fillText('The autumn roasts', 80, 190);
g.font = '36px Georgia, serif';
g.fillText('Ember & Oak Coffee', 84, 250);
return c.toDataURL('image/jpeg', 0.8);
})();
designer.fields = [
{ name: 'first_name', label: 'First name', sample: 'Ada' },
{ name: 'shop_url', label: 'Shop link', sample: 'https://example.com/shop' },
{ name: 'unsubscribe_url', label: 'Unsubscribe link', sample: 'https://example.com/unsubscribe' },
];
designer.imageHandler = file =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result));
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(file);
});
designer.value = {
subject: 'The autumn roasts are here, {{first_name|friend}}',
preheader: 'Three new single origins, roasted this week',
settings: { width: 600, background: '#f3eee8', contentBackground: '#ffffff', fontFamily: "Georgia, 'Times New Roman', serif", fontSize: 16, textColor: '#3b302a', headingColor: '#2a1d16', linkColor: '#a2481c' },
rows: [
{ id: 'r1', layout: [1], columns: [[{ id: 'b1', type: 'image', src: banner, alt: 'The autumn roasts from Ember & Oak Coffee', href: '{{shop_url}}', align: 'center' }]] },
{
id: 'r2',
layout: [1],
padding: [8, 0],
columns: [
[
{ id: 'b2', type: 'heading', text: 'Hello {{first_name|friend}},', level: 1, align: 'left', padding: [24, 32] },
{ id: 'b3', type: 'text', text: 'Three new single origins arrived this week, and we have roasted them **light enough to taste the fruit**.\n\nEach bag is roasted to order and sent the next morning. [See them all](https://example.com/shop).', align: 'left', padding: [0, 32] },
{ id: 'b4', type: 'button', label: 'Shop the autumn roasts', href: '{{shop_url}}', align: 'left', background: '#a2481c', color: '#ffffff', radius: 4, fullWidth: false, padding: [24, 32] },
],
],
},
{
id: 'r3',
layout: [1, 1, 1],
background: '#faf6f1',
padding: [16, 16],
columns: [
[{ id: 'b5', type: 'heading', text: 'Ethiopia Guji', level: 3, align: 'center', padding: [8, 12] }, { id: 'b6', type: 'text', text: 'Peach, jasmine and bergamot.', align: 'center', padding: [0, 12] }],
[{ id: 'b7', type: 'heading', text: 'Colombia Huila', level: 3, align: 'center', padding: [8, 12] }, { id: 'b8', type: 'text', text: 'Red apple, caramel, cocoa.', align: 'center', padding: [0, 12] }],
[{ id: 'b9', type: 'heading', text: 'Rwanda Nyamasheke', level: 3, align: 'center', padding: [8, 12] }, { id: 'b10', type: 'text', text: 'Blackcurrant and brown sugar.', align: 'center', padding: [0, 12] }],
],
},
{ id: 'r4', layout: [1], columns: [[{ id: 'b11', type: 'divider', color: '#e3d9cf', thickness: 1, padding: [24, 32] }, { id: 'b12', type: 'social', links: [{ label: 'View in your browser', href: 'https://example.com/email' }, { label: 'Unsubscribe', href: '{{unsubscribe_url}}' }], align: 'center', padding: [0, 32] }, { id: 'b13', type: 'spacer', height: 24 }]] },
],
};
designer.addEventListener('bmxChange', async () => {
const problems = await designer.check();
const errors = problems.filter(p => p.severity === 'error').length;
out.textContent = errors ? `${errors} thing(s) to fix before sending.` : `Ready to send: ${Math.round((await designer.getHtml()).length / 1024)} KB of HTML.`;
});
</script>
<bmx-email-designer label="Autumn newsletter"></bmx-email-designer>
<script type="module">
const designer = document.querySelector('bmx-email-designer');
designer.fields = [{ name: 'first_name', label: 'First name', sample: 'Ada' }];
designer.addEventListener('bmxChange', e => save(e.detail.document));
const html = await designer.getHtml(); // for your sending service
const text = await designer.getText();
</script>
Merge fields ({{first_name}}, {{first_name|there}}) stay in the HTML
for the sending service to fill; the preview can fill them with samples.
The design is plain JSON (value, bmxChange). Checks list what to fix
before sending: no subject, images without descriptions, links that go
nowhere, faint text, unknown merge fields, and size over Gmail's limit.
Nothing leaves the page: images are addresses the page gives, or files
handed to imageHandler to store where the page chooses.
Keyboard: the content is a tree of rows and blocks. Arrow keys move through it; Alt+Up and Alt+Down move the selected row or block (a block goes on into the next column); Delete removes; Control+D duplicates; Enter goes to its settings; Control+Z and Control+Y undo and redo. Blocks and rows are added from the Add list, by click or Enter, or by dragging.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
fields |
fields |
BmxEmailField[] | string |
— | Merge fields to offer, as an array or JSON: { name, label, sample }. When given, others are flagged. |
imageHandler |
property only | (file: File) => Promise<string> |
— | Stores an image file and resolves to its address. When set, image blocks offer an upload. |
label |
label |
string |
— | The design's name, shown in the toolbar and given to assistive technology. |
previewData |
preview-data |
Record<string, string> | string |
— | Values for the preview's merge fields, over the fields' samples. |
readonly |
readonly |
boolean |
false |
Look but do not change. |
strings |
strings |
Partial<BmxEmailDesignerStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
value |
value |
BmxEmailDocument | string |
— | The design, as an object or JSON. Two-way: edits set it and fire bmxChange. |
Events
| Event | Detail | Description |
|---|---|---|
bmxChange |
BmxEmailChangeDetail |
The design changed. |
Methods
| Method | Signature | Description |
|---|---|---|
addBlock |
addBlock(type: BmxEmailBlockType) => Promise<string | null> |
Adds a block after the selected one (or a row, if nothing is selected). Returns its id. |
addRow |
addRow(layout?: string) => Promise<string | null> |
Adds a row of columns ('1', '1-1', '1-2', '2-1', '1-1-1') after the selected row. Returns its id. |
check |
check() => Promise<BmxEmailProblem[]> |
What to fix before sending. |
getDocument |
getDocument() => Promise<BmxEmailDocument> |
The design. |
getHtml |
getHtml(options?: BmxEmailExportOptions) => Promise<string> |
The HTML to send. Merge fields stay as written unless data is given. |
getText |
getText(options?: BmxEmailExportOptions) => Promise<string> |
The plain-text part to send with the HTML. |
redo |
redo() => Promise<void> |
|
select |
select(id: string | null) => Promise<void> |
Selects a row or block by id (or nothing). |
setDocument |
setDocument(doc: BmxEmailDocument | string) => Promise<void> |
Replaces the design (checked and cleaned as value is). Undo history starts again. |
undo |
undo() => Promise<void> |
CSS shadow parts
| Part | Description |
|---|---|
canvas |
The email as it is designed. |
inspector |
The settings panel. |
palette |
The Add list. |
toolbar |
The bar of commands. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-email-designer-drop |
The line showing where a dragged block will go. |
--bmx-email-designer-height |
The height of the panes. |
--bmx-email-designer-inspector-width |
The settings panel's width. |
--bmx-email-designer-outline |
The outline of the selected row or block. |
--bmx-email-designer-palette-width |
The Add list's width. |
--bmx-email-designer-radius |
Corner radius of the frame. |