@emabuild/core
v0.2.0
Published
Drag & drop email editor Web Component — embeddable anywhere
Downloads
1,594
Maintainers
Readme
@emabuild — Drag & Drop Email Editor
A fully embeddable drag-and-drop email editor Web Component. Cross-client email HTML export, and 13 built-in content blocks.
Built with Lit 3 — works in Angular, React, Vue, or plain HTML.
Quick Start
npm install @emabuild/core lit<mail-editor id="editor" style="height:100vh;"></mail-editor>
<script type="module">
import '@emabuild/core/mail-editor';
const editor = document.getElementById('editor');
editor.addEventListener('editor:ready', () => {
console.log('Editor is ready');
});
</script>Packages
| Package | Description | Size |
|---------|-------------|------|
| @emabuild/core | <mail-editor> Web Component | ~14.6KB gzip |
| @emabuild/email-renderer | Standalone HTML export engine (works server-side) | ~2.7KB gzip |
| @emabuild/types | TypeScript type definitions | types only |
Framework Integration
Angular
npm install @emabuild/core lit// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}// app.component.ts
import '@emabuild/core/mail-editor';
@Component({
template: `<mail-editor #editor style="height:100vh;"></mail-editor>
<button (click)="exportHtml()">Export</button>`,
})
export class AppComponent {
@ViewChild('editor') editor!: ElementRef;
exportHtml() {
this.editor.nativeElement.exportHtml((result) => {
console.log(result.html);
});
}
}React
import '@emabuild/core/mail-editor';
import { useRef, useEffect } from 'react';
export function EmailEditor() {
const editorRef = useRef<any>(null);
useEffect(() => {
const el = editorRef.current;
el?.addEventListener('editor:ready', () => {
el.loadDesign(myDesign);
});
}, []);
const handleExport = () => {
editorRef.current?.exportHtml((result) => {
console.log(result.html);
});
};
return (
<div style={{ height: '100vh' }}>
<mail-editor ref={editorRef} />
<button onClick={handleExport}>Export HTML</button>
</div>
);
}Vue
<template>
<mail-editor ref="editor" style="height: 100vh" />
<button @click="exportHtml">Export</button>
</template>
<script setup>
import '@emabuild/core/mail-editor';
import { ref, onMounted } from 'vue';
const editor = ref(null);
onMounted(() => {
editor.value.addEventListener('editor:ready', () => {
editor.value.loadDesign(myDesign);
});
});
function exportHtml() {
editor.value.exportHtml((result) => {
console.log(result.html);
});
}
</script>API Reference
Methods
// Load a design JSON
editor.loadDesign(design: EmailDesign): void;
// Save the current design as JSON
editor.saveDesign(callback: (design: EmailDesign) => void): void;
// Export email-ready HTML
editor.exportHtml(callback: (result: ExportResult) => void, options?: ExportOptions): void;
// Promise-based export
const result = await editor.exportHtmlAsync(options?: ExportOptions);
// Undo / Redo
editor.undo(): void;
editor.redo(): void;
// Register a custom tool
editor.registerTool(definition: ToolDefinition): void;
// Register a callback (e.g. for image upload)
editor.registerCallback(type: string, callback: Function): void;
// Update body-level settings
editor.setBodyValues(values: Partial<BodyValues>): void;Events
editor.addEventListener('editor:ready', () => {
// Editor is fully initialized
});
editor.addEventListener('design:loaded', (e) => {
// A design was loaded via loadDesign()
console.log(e.detail.design);
});
editor.addEventListener('design:updated', (e) => {
// User made a change
console.log(e.detail.type); // 'content_updated' | 'row_added' | ...
});Export Result
editor.exportHtml((result) => {
result.design; // EmailDesign JSON — save this for future editing
result.html; // Complete HTML document (<!DOCTYPE> to </html>)
result.chunks; // { body, css, fonts[], js }
});Export Options
editor.exportHtml(callback, {
minify: true, // Minify HTML output
inlineStyles: true, // Inline CSS into style attributes
cleanup: true, // Remove unused CSS classes
mergeTags: { // Replace merge tags with values
first_name: 'John',
company: 'Acme',
},
});Built-in Content Blocks
| Block | Description | Email Safe | |-------|-------------|:----------:| | Text | Rich text with formatting | Yes | | Heading | H1-H4 with size/weight/color | Yes | | Paragraph | Block text with line-height | Yes | | Image | Responsive image with link | Yes | | Button | CTA button (VML Outlook fallback) | Yes | | Divider | Horizontal line | Yes | | HTML | Raw HTML injection | Yes | | Social | Social media icon links | Yes | | Menu | Horizontal navigation | Yes | | Video | YouTube/Vimeo thumbnail + play | Yes | | Timer | Countdown display | Yes | | Table | Data table with headers | Yes | | Form | Input form (web mode only) | Web only |
Layout Presets
Drag or click to add rows with predefined column layouts:
100%— single column50 / 50— two equal columns33 / 33 / 33— three equal columns66 / 33— two-thirds + one-third33 / 66— one-third + two-thirds25 / 25 / 25 / 25— four equal columns
Custom Tools
Register custom content blocks with their own properties and renderers:
import { html } from 'lit';
editor.registerTool({
name: 'product_card',
label: 'Product Card',
icon: '<svg>...</svg>',
supportedDisplayModes: ['email'],
options: {
product: {
title: 'Product',
options: {
name: { label: 'Name', defaultValue: 'Product', widget: 'text' },
price: { label: 'Price', defaultValue: '$0.00', widget: 'text' },
image: { label: 'Image URL', defaultValue: '', widget: 'text' },
bgColor: { label: 'Background', defaultValue: '#ffffff', widget: 'color_picker' },
},
},
},
defaultValues: {
name: 'Product', price: '$0.00', image: '', bgColor: '#ffffff',
},
renderer: {
renderEditor(values) {
return html`
<div style="background:${values.bgColor};padding:16px;text-align:center;">
<img src="${values.image}" style="max-width:100%;border-radius:8px;" />
<h3 style="margin:12px 0 4px;">${values.name}</h3>
<p style="color:#3b82f6;font-weight:bold;">${values.price}</p>
</div>
`;
},
renderHtml(values, ctx) {
return `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td style="background-color:${values.bgColor};padding:16px;text-align:center;">
<img src="${values.image}" width="${ctx.columnWidth}" style="max-width:100%;" />
<h3 style="margin:12px 0 4px;">${values.name}</h3>
<p style="color:#3b82f6;font-weight:bold;">${values.price}</p>
</td></tr>
</table>`;
},
},
});Design JSON Structure
Designs are stored as structured JSON. The format is interoperable with other email editor tools.
{
"counters": { "u_row": 1, "u_column": 1, "u_content_text": 1 },
"body": {
"id": "u_body",
"rows": [
{
"id": "u_row_1",
"cells": [1],
"columns": [
{
"id": "u_column_1",
"contents": [
{
"id": "u_content_text_1",
"type": "text",
"values": {
"text": "<p>Hello World</p>",
"containerPadding": "10px",
"_meta": { "htmlID": "u_content_text_1", "htmlClassNames": "u_content_text" }
}
}
],
"values": { "backgroundColor": "", "padding": "0px", "_meta": { "htmlID": "u_column_1", "htmlClassNames": "u_column" } }
}
],
"values": { "backgroundColor": "", "columnsBackgroundColor": "#ffffff", "padding": "0px", "_meta": { "htmlID": "u_row_1", "htmlClassNames": "u_row" } }
}
],
"headers": [],
"footers": [],
"values": {
"backgroundColor": "#e7e7e7",
"contentWidth": "600px",
"fontFamily": { "label": "Arial", "value": "arial,helvetica,sans-serif" },
"textColor": "#000000",
"preheaderText": "",
"_meta": { "htmlID": "u_body", "htmlClassNames": "u_body" }
}
},
"schemaVersion": 16
}Server-Side Rendering
Use @emabuild/email-renderer to generate HTML from design JSON on the server (Node.js):
import { renderDesignToHtml } from '@emabuild/email-renderer';
const toolRenderers = new Map();
// Register tool HTML renderers...
const result = renderDesignToHtml(designJson, toolRenderers, {
minify: true,
mergeTags: { first_name: 'John' },
});
console.log(result.html); // Complete email HTMLEmail Client Support
Exported HTML uses fluid hybrid design with MSO conditional comments for maximum compatibility:
- Gmail (Web, iOS, Android)
- Outlook (2016, 2019, 365, Outlook.com)
- Apple Mail (macOS, iOS)
- Yahoo Mail
- Thunderbird
- Samsung Mail
Key techniques used:
- Table-based layout with
role="presentation" - MSO ghost tables (
<!--[if mso]>) for Outlook column rendering - VML roundrect for Outlook button border-radius
- Responsive CSS with
@mediaqueries - Fluid hybrid columns (
display:inline-block+max-width) - Dark mode support (
prefers-color-scheme) - Preheader text with invisible padding fill
Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| Ctrl/Cmd + Z | Undo |
| Ctrl/Cmd + Y | Redo |
| Ctrl/Cmd + Shift + Z | Redo |
| Delete / Backspace | Delete selected element |
| Escape | Deselect |
Property Widgets
Available widget types for tool property definitions:
| Widget | Type | Description |
|--------|------|-------------|
| text | string | Single-line text input |
| rich_text | string | Multi-line textarea (HTML) |
| color_picker | string | Color swatch + hex input |
| toggle | boolean | Checkbox toggle |
| dropdown | string | Select dropdown (requires widgetParams.options) |
| alignment | string | Left/center/right visual picker |
| padding | string | 4-side padding editor (CSS shorthand) |
Performance
The editor is optimized for speed, even with large designs (100+ rows):
- Fine-grained reactivity — Components subscribe only to the store channels they need. A hover event re-renders only the affected block, not the entire tree.
- Lazy tool loading — 7 secondary tools are loaded on-demand via dynamic imports. The initial bundle contains only the 6 most-used tools. All lazy tools are preloaded during browser idle time via
requestIdleCallback. - Minified bundle — esbuild minification reduces the core package to ~63KB (14.6KB gzip).
| Metric | Value | |--------|-------| | Initial bundle | 63KB min / 14.6KB gzip | | Lazy tool chunks | 7 × ~3KB each | | Hover re-renders | O(1) — only the affected block | | Time to interactive | < 200ms |
Development
# Install dependencies
pnpm install
# Start the demo app
pnpm --filter @emabuild/demo dev
# Build all packages
pnpm build
# Publish to npm
pnpm -r publish --access publicLicense
MIT
