@naturaldevcr/vue-mail-designer
v0.3.8
Published
A visual drag & drop email builder for Vue 3. Exports email-client-compatible HTML and a re-editable design JSON.
Maintainers
Readme
@naturaldevcr/vue-mail-designer
A visual drag & drop email builder for Vue 3. Generates email-client-compatible HTML and a re-editable design JSON.
📖 Full documentation · Repository · 🤖 llms-full.txt for AI coding assistants
Install
pnpm add @naturaldevcr/vue-mail-designer vue piniaBasic usage
<template>
<EmailBuilder
v-model:design="design"
:merge-tags="mergeTags"
:upload-image="uploadImage"
:media-library="mediaLibrary"
:autosave="autosave"
@export-html="onHtml"
@autosave-error="onAutosaveError"
/>
</template>
<script setup lang="ts">
import {
EmailBuilder,
type AutosaveErrorPayload,
type AutosaveOptions,
type EmailDocument,
type MergeTagDef,
} from '@naturaldevcr/vue-mail-designer'
import '@naturaldevcr/vue-mail-designer/style.css'
import { ref } from 'vue'
const design = ref<EmailDocument>()
const mergeTags: MergeTagDef[] = [{ name: 'First name', value: 'first_name' }]
const autosave: AutosaveOptions = {
enabled: true,
storage: { type: 'local', key: 'campaign:spring-launch:draft' },
restore: true,
}
async function uploadImage(file: File): Promise<string> {
// upload the file to your CDN and return the URL
return 'https://cdn.your-domain.com/...'
}
const mediaLibrary = {
async list(cursor?: string) {
// list your bucket, paginated by cursor
return { items: [], nextCursor: undefined }
},
async upload(file: File) {
// upload the file and return the full MediaItem (id, url, thumbnailUrl, name)
return { id: 'x', url: '...', thumbnailUrl: '...', name: file.name }
},
async delete(id: string) {
// delete the file from your bucket
},
async rename(id: string, name: string) {
// rename the file and return the updated MediaItem
return { id, url: '...', thumbnailUrl: '...', name }
},
}
function onHtml(html: string) {
// save or send the HTML
}
function onAutosaveError(payload: AutosaveErrorPayload) {
// report autosave failures
}
</script>Localization
English is the default UI language. Use locale="es" to switch the builder to Spanish, or pass a partial LocaleDict to override only the English labels you want to customize.
<EmailBuilder locale="en" />
<EmailBuilder locale="es" /><script setup lang="ts">
import { EmailBuilder, type LocaleDict } from '@naturaldevcr/vue-mail-designer'
const partialLocale: LocaleDict = {
'images.gallery': 'Brand library',
'image.searchPlaceholder': 'Search product photos',
}
</script>
<template>
<EmailBuilder :locale="partialLocale" />
</template>Any key you do not provide still falls back to the built-in English dictionary.
To hide the builder header while using distinct colors for each mode:
<EmailBuilder
:show-header="false"
theme="dark"
:appearance="{
light: { accent: '#2563eb', panel: '#ffffff' },
dark: { accent: '#60a5fa', panel: '#111827' },
}"
/>The flat Appearance form remains supported. When the header is hidden, the header-only template, saved-status, and theme-toggle controls are removed; the Export actions remain available in the right rail, and the host app can also use the component's methods and events.
Autosave
Autosave is optional and uses the public autosave prop:
type AutosaveOptions = {
enabled: boolean
storage: AutosaveStorage
mode?: 'change' | 'debounce' | 'interval'
delay?: number
restore?: boolean
restorePrecedence?: 'initial-design' | 'saved-design'
}restore defaults to false (off), so a saved draft does not replace the initial design unless restoration is enabled.
The two supported storage shapes are:
type AutosaveStorage =
| {
type: 'local'
key: string
storage?: Storage
}
| {
type: 'custom'
load?: () => Promise<EmailDocument | undefined> | EmailDocument | undefined
save: (document: EmailDocument) => Promise<void> | void
}modedefaults to'debounce'delaydefaults to1000for'debounce',5000for'interval', and0for'change'restorePrecedencedefaults to'initial-design'- save-only custom adapters are supported because
loadis optional
Use a stable local-storage key for the same host record, such as campaign:${campaignId}:draft. For custom storage, the library only calls your load and save functions; retention, deletion, conflict resolution, and any remote cleanup remain the host application's responsibility.
Listen for the autosave lifecycle through autosave-status, autosave-saved, autosave-restored, and autosave-error, and read the current lifecycle state through getAutosaveStatus().
See the full Autosave guide.
Props
| Prop | Type | Description |
|------|------|-------------|
| design | EmailDocument | Design (v-model). |
| mergeTags | MergeTagDef[] | Variables insertable in text ({ name, value }). |
| templates | EmailTemplate[] | Extra templates, in addition to the built-in ones. |
| uploadImage | (file: File) => Promise<string> | Upload handler; returns the final URL. |
| imageSearch | (query: string) => Promise<ImageResult[]> | Search handler for the Search subtab in the unified Images panel; defaults to openverseSearch. |
| mediaLibrary | MediaLibraryOptions | Enables the Gallery subtab in the unified Images panel: { list: (cursor?) => Promise<{ items: MediaItem[], nextCursor? }>, upload: (file) => Promise<MediaItem>, delete: (id) => Promise<void>, rename: (id, name) => Promise<MediaItem> }. Without this prop, only Search is shown. Every function is implemented by the integrator against their own storage (e.g. Firebase Storage); the library assumes no particular backend. |
| timerImageUrlBuilder | (block: TimerBlock) => string \| undefined | Optional email-safe timer image provider. Called during preview and HTML export when block.imageUrl is empty; return a remotely served GIF or generated image URL. Without it, exported timers use a static snapshot because email clients cannot run a live countdown. An explicit block.imageUrl always takes precedence. |
| socialIconUrlBuilder | (kind: SocialNetworkKind) => string \| undefined | Optional email asset provider for social icons. Exported email HTML uses hosted HTTPS icon URLs by default; return a self-hosted URL to control caching, privacy, and availability. Empty or throwing callbacks fall back to the built-in URL. |
| unlayerFetch | (slug: string) => Promise<unknown> | Handler to load an Unlayer template by URL/slug; returns the design JSON. Defaults to hitting Unlayer's API directly (fails via CORS without a proxy). |
| theme | 'light' \| 'dark' | Builder UI theme. |
| showHeader | boolean | Whether to show the builder header. Defaults to true; when false, the entire builder header is hidden. |
| locale | 'en' \| 'es' \| LocaleDict | Public UI language option. English ('en') is the default, Spanish ('es') is the built-in alternative, and a LocaleDict is merged on top of English so you can override only the keys you want. |
| appearance | Appearance \| ThemeAppearance | Builder colors. A flat object ({ accent, panel, border, background, foreground, muted }) applies to both modes. The union Appearance or ThemeAppearance also accepts { light?: Appearance, dark?: Appearance } for mode-specific values; omitted fields keep that mode's defaults. |
| ai | AiOptions | Optional Chrome built-in AI tools for the rich text editor: { enabled: boolean, languages?: AiLanguage[] }. The menu is rendered only when enabled; languages configures Translate targets. Browser API availability is checked at runtime. |
| autosave | AutosaveOptions | Optional draft persistence with local or custom storage, configurable save timing, optional restore, and precedence control. |
| tools | Partial<Record<BlockType, ToolConfig>> | Per-block palette config: { enabled?, position?, usageLimit? } to hide, reorder, or limit instances. |
| fonts | FontDef[] | List of fonts ({ label, value, url? }); Google Fonts (url) are loaded both in the canvas and in the exported HTML. Defaults to a curated list. |
| specialLinks | SpecialLink[] | Special links insertable from the editor ({ name, href }, e.g. an unsubscribe link). |
| customBlocks | CustomBlockDef[] | Integrator-defined custom blocks ({ type, label, icon?, defaultData, fields, render }); appear in the palette with a generic inspector, shared outer Top/Right/Bottom/Left padding, and their own render in the export. |
mergeTags also accepts groups: { name, tags: MergeTagDef[] } (shown as optgroups in the editor).
Custom block security: your
render(data)generates raw HTML. Ifdatacan come from an imported JSON, escape the values (the library exportsescapeHtml) to avoid injection.
Images panel
The builder uses one unified Images panel:
- Gallery uses
mediaLibraryto show your uploaded assets. When configured, it is the first and default subtab. - Search uses
imageSearch(or the built-inopenverseSearch) to find external images. WithoutmediaLibrary, Search is the only subtab.
Clicking a thumbnail opens a preview dialog first. Choose Add to insert a new Image block on the canvas or replace the currently selected Image block. You can also drag thumbnails directly from Search or Gallery onto the canvas, onto an existing Image block, or onto a Gallery block slot.
Email-safe timers
Email clients cannot run a reliable JavaScript or CSS countdown inside an exported message. For a live timer, provide a remotely served GIF or dynamically generated image through timerImageUrlBuilder:
const timerImageUrlBuilder = (block: TimerBlock) =>
`https://your-domain.example/email-timer.gif?end=${encodeURIComponent(block.endDate)}`<EmailBuilder :timer-image-url-builder="timerImageUrlBuilder" />The callback receives the complete TimerBlock, so the host can use endDate and any campaign context available to its image service. If neither imageUrl nor the callback returns a URL, the export uses the static timer fallback; the editor countdown remains live while editing.
Extra methods (via ref)
exportHtml(): string— full email HTML ready for a sending provider.exportJson(): string— the current editable design serialized as JSON.getDesign(): EmailDocument— the current editable design object.loadDesign(doc): void— replace the current design programmatically.exportImage(): Promise<string>— PNG (data URL) of the design. Limitation: cross-origin images (CORS) can prevent the capture.- The Export tab in the right rail provides HTML, JSON, JSON import, Unlayer import, PNG, and version actions. Versions are saved/loaded/deleted in memory for the session.
Email-safe social icons
Email clients commonly block data: image sources. Social blocks therefore use normal hosted HTTPS icon URLs in exported HTML while keeping inline SVG icons in the editor canvas. The default URLs use hosted Simple Icons-compatible assets; for production email delivery, self-host the files and provide a stable URL builder:
const socialIconUrlBuilder = (kind: SocialNetworkKind) =>
`https://cdn.example.com/email-icons/${kind}.svg`<EmailBuilder :social-icon-url-builder="socialIconUrlBuilder" />The callback is used only for exported email HTML. The colored link background and accessible network alt text remain in the generated markup.
Backgrounds
The body background color and image are edited in the Body tab (settings.backgroundColor / settings.backgroundImage). Rows are transparent by default so the body background shows through; each row and column can have its own background color/image.
Rich text editor
The rich text editor includes bold, italic, underline, strikethrough, lists (bullet/numbered), alignment, text color, font size, links, variables (merge tags), and clear formatting.
Chrome AI tools
Set ai.enabled to true to show optional Chrome built-in AI actions in the rich text toolbar. The menu supports Rewrite, Write, Summarize, and Translate. Configure Translate targets with languages: [{ code, label }]. Selection-based actions require selected text, results remain uncommitted until Apply, and unavailable browser APIs disable the corresponding action. Chrome manages the local model cache; Summarizer sessions are reused for the same configuration during the page lifetime and are recreated after a failure.
See the Chrome AI tools guide for the full configuration and availability behavior.
Importing from Unlayer
From the Export → Import from Unlayer… menu you can paste an Unlayer design JSON, or the URL of a template from their studio (e.g. https://studio.unlayer.com/create/...). The design is converted to our format and a list of warnings is shown for anything that couldn't be mapped (responsive-specific styles, display conditions, Google fonts, etc.).
- Programmatic:
unlayerToDocument(json)returns{ document, warnings };unlayerSlugFromUrl(url)extracts the slug. - By URL: the browser can't hit Unlayer's API directly due to CORS. Pass an
unlayerFetchthat uses your own backend/proxy (the demo app uses a Vite proxy at/unlayer-api). - Assets: images from Unlayer templates live on their CDN and belong to them; replace them with your own assets. The converter warns about this automatically.
Events
update:design— on every design change.change— same as above, for when you'd rather not usev-model.export-html— when callingexportHtml(); delivers the HTML.autosave-status— emits{ status, error? }when autosave changes lifecycle state.autosave-saved— emits{ design, savedAt }after a successful save.autosave-restored— emits{ design, restoredAt }after a saved draft is applied.autosave-error— emits{ operation: 'load' | 'save', error }after a load or save failure.
Methods (via ref)
exportHtml(): stringexportJson(): stringgetDesign(): EmailDocumentloadDesign(doc: EmailDocument): voidgetAutosaveStatus(): AutosaveStatus
Blocks
Heading, Text (rich editor), Image, Button, Divider, Spacer, Social, Menu, HTML, Video, Table, Gallery, and Timer (countdown: integrator-provided dynamic image, or a static box with the days remaining).
Rich properties
- Hide per device per block and per row (
hideDesktop/hideMobile) — the exported HTML uses classes + a media query. - Background image per row (
url,repeat,size,position). - Own font per heading/text block (in addition to the document font).
- Border and radius per column (supported in the model and the HTML; no dedicated inspector control yet).
- Image cropping ("Crop" button in an image block's inspector, only visible with
uploadImageconfigured) — aspect ratio, rotate/flip, straighten, and corner radius (borderRadius); the result is uploaded viauploadImage. - Timer styling — static timers expose card background, border, corner radius, number/label colors, font family, and editable Days/Hours/Minutes/Seconds labels in the inspector.
Email compatibility
The HTML uses tables with inline styles, ghost tables for Outlook, and a media query to stack columns on mobile. Avoids flex/grid/position.
Limitations
- Doesn't import existing HTML (JSON only).
- Row backgrounds in Outlook: partial support (no full-bleed VML yet).
- Merge tags are emitted as
{{value}}; your platform's engine replaces them. - Columns can't be reordered within a row (rows and blocks can).
themeonly accepts'light' | 'dark'(no'auto').- Column border/radius has no dedicated inspector control yet; the timer doesn't animate without an integrator-provided image service.
- The image block's corner radius (
borderRadius) is rendered with CSSborder-radius; Outlook desktop (Word engine) ignores it, so rounded corners look right in the builder and in most clients but not in Outlook desktop.
