@vitrina/email-builder
v0.1.4
Published
Unlayer-style drag-and-drop email builder for React. Rows/columns/blocks (text, heading, button, image, divider, spacer, social, menu, HTML), inline rich-text, undo/redo, desktop/mobile preview. Emits email-safe, dark-mode-proof table HTML. Framework-agno
Readme
@vitrina/email-builder
An Unlayer-style drag-and-drop email builder for React. Rows → weighted
Columns → Blocks (text, heading, button, image, divider, spacer, social, menu,
HTML). Inline rich-text, undo/redo, desktop/mobile preview. It edits a plain
EmailDesign JSON object and compiles it to email-safe table HTML — inline
styles, forced-light rendering, fluid columns that stack on phones, and Outlook
fallbacks.
- Framework-agnostic — one
<EmailBuilder value onChange />component. - Tailwind + shadcn friendly — styled entirely with utility classes + your CSS-var theme tokens. No stylesheet to import.
- i18n-free — every visible string is injected via a
labelsprop. - Merge-tag aware —
{{name}}-style tags pass through untouched, for your own per-recipient rendering. - Tiny — no editor engine bundled; only
highlight.js+lucide-reactas deps, React as a peer.
Built by Vitrina for its campaigns product and extracted as a standalone package.
Install
npm i @vitrina/email-builder
# or: pnpm add @vitrina/email-builder / yarn add @vitrina/email-builderPeer deps: React ≥ 19 and react-dom.
Quick start
import { useState } from 'react';
import { EmailBuilder, compileDesign, defaultDesign, type EmailDesign } from '@vitrina/email-builder';
export function MyEditor() {
const [design, setDesign] = useState<EmailDesign>(() => defaultDesign());
return (
<div style={{ height: 640 }}>
<EmailBuilder
value={design}
onChange={setDesign}
uploadImage={async (file) => uploadAndReturnPublicUrl(file)} // optional
socialIconBase="https://cdn.example.com/social-icons" // optional
/>
</div>
);
}
// When you save/send — compileDesign returns the <body> inner HTML. Wrap it in
// your own shell (doctype/head + compliance footer) and render merge tags
// server-side per recipient.
const bodyHtml = compileDesign(design, { socialIconBase: 'https://cdn.example.com/social-icons' });Styling requirements
The builder renders Tailwind utility classes and reads shadcn/ui-style CSS
variables (--primary, --muted-foreground, --border, --popover,
--ring, --destructive, …). Any shadcn base provides them.
Because the classes live inside the compiled package, tell Tailwind to scan it.
Tailwind v4 (globals.css):
@import "tailwindcss";
@source "../node_modules/@vitrina/email-builder/dist";Tailwind v3 (tailwind.config):
content: ['./src/**/*.{ts,tsx}', './node_modules/@vitrina/email-builder/dist/**/*.js'],The HTML block's code editor picks up syntax colors from any highlight.js theme you happen to load — optional; it degrades to monochrome without one.
API
| Export | What |
| --- | --- |
| EmailBuilder | The editor. Props below. |
| compileDesign(design, opts?) | EmailDesign → email-safe body HTML string. opts.socialIconBase for hosted social icons. |
| defaultDesign() | A pleasant starter design. |
| blockCount(design) | Total blocks (e.g. to gate "next"). |
| MOBILE_STACK_CSS | Optional <style> for your <head> (extra mobile polish). |
| DEFAULT_LABELS, BuilderLabels | The English label bag + its type. |
| types | EmailDesign, EmailSettings, Block, BlockType, Row |
<EmailBuilder> props
| Prop | Type | |
| --- | --- | --- |
| value | EmailDesign | required — the design (controlled) |
| onChange | (d: EmailDesign) => void | required |
| labels? | Partial<BuilderLabels> | override any string (defaults are English) |
| uploadImage? | (file: File) => Promise<string> | host-provided uploader; without it images are URL-only |
| socialIconBase? | string | absolute base for hosted icons — <base>/<slug>.png; omit for a colored-chip fallback |
| height? | number \| string | editor height (default 640) |
The compile contract
compileDesign emits the <body> inner HTML only. You own the outer
shell — doctype/<head>, and any compliance footer (unsubscribe, physical
address) your emails need. Merge tags like {{name}} are left verbatim so you
can render them per recipient at send time.
Rules the output follows so it renders across clients:
- Table-based layout, 100% inline styles (no
<style>/media-queries in the body). - Forced light friendliness — pair with
<meta name="color-scheme" content="light only">in your head to avoid dark-mode inversion breaking your colors. - Fluid columns — multi-column rows use the inline-block "hybrid" pattern, so they stack on phones with no media query (Gmail included), with an
[if mso]ghost table keeping Outlook side-by-side. - Images carry a
widthattribute (Outlook sizes off it) +max-width:100%.
Email-client notes
| | Gmail | Apple Mail | Outlook (desktop) |
| --- | --- | --- | --- |
| Core layout, buttons, images | ✅ | ✅ | ✅ |
| Column stacking on mobile | ✅ | ✅ | n/a |
| border-radius (rounded buttons/icons) | ✅ | ✅ | ⬜ renders square |
| Row background images | ✅ | ✅ | ⬜ needs VML |
Social icons need publicly-hosted PNGs at socialIconBase (Gmail/Outlook strip
SVG). Slugs: instagram facebook whatsapp x twitter linkedin youtube tiktok
pinterest snapchat telegram messenger reddit spotify threads github vimeo tumblr
discord email website.
Development
pnpm install
pnpm test # vitest — compiler + model unit tests
pnpm typecheck
pnpm lint
pnpm build # tsup → dist (ESM + d.ts)
# runnable playground (see examples/basic):
cd examples/basic && pnpm install && pnpm devSee CONTRIBUTING.md for the full workflow and CHANGELOG.md for release history.
License
MIT © Vitrina
