Skip to content

Repository files navigation

mailc

A modern email markup compiler — attribute or Tailwind styling, email-safe, developer-first with native dynamic templating.

Write emails as components. Compile to HTML that works in Gmail, Outlook, Apple Mail, and the rest of the long tail. Use Tailwind classes if you want; use plain CSS attributes if you don't.

<mc><mc-body><mc-sectionpadding="32px 0"><mc-column><mc-textfont-size="24px" font-weight="bold" align="center">
Welcome aboard!
</mc-text><mc-buttonhref="https://example.com" background-color="#0066cc" color="#ffffff">
Get Started
</mc-button></mc-column></mc-section></mc-body></mc>
import{compile}from'mailc'const{ html, errors, warnings }=compile(source)// html is ready to send. errors/warnings tell you what's broken.

That's the whole API for the common case.

Status: early. The compiler is solid (3,700+ tests, all passing) but the user base is small. If you try it and hit something rough, please open an issue — feedback at this stage is the most useful contribution you can make.


Why mailc

Email HTML is awful. You can't use flexbox, grid, or modern CSS. You need tables nested inside tables. Outlook needs separate VML markup for backgrounds. Gmail strips <style> blocks under certain conditions. mailc handles all of that and gives you a clean component model on top.

You'd reach for mailc when:

  • You want a TypeScript-native compiler with structured errors, not a black-box CLI
  • You want to use Tailwind utilities, attribute-based styling, or both
  • You want to build a tool on top of an email compiler — JSON IR, source maps, plugin API, AI-agent integration are first-class

Install

npm install mailc
# or
pnpm add mailc
# or
yarn add mailc

Requires Node 20+ for the CLI / Node API. The browser bundle works in any modern browser.


Quick start

1. Compile from a string

import{compile}from'mailc'constresult=compile(` <mc> <mc-body> <mc-section> <mc-column> <mc-text>Hello {{user.name}}</mc-text> </mc-column> </mc-section> </mc-body> </mc>`,{data: {user: {name: 'Ada'}},})console.log(result.html)// production-ready HTMLconsole.log(result.errors)// [] when cleanconsole.log(result.warnings)// non-blocking issues

2. Compile from JSON (for builder UIs)

import{compileFromJSON}from'mailc'constresult=compileFromJSON({type: 'mc',children: [{type: 'mc-body',children: [/* ... */],},],})

3. CLI

mailc build welcome.mc -o welcome.html
mailc validate welcome.mc
mailc watch welcome.mc -o welcome.html --serve # live-reload preview server
mailc init # scaffold a new project
mailc contract welcome.mc # show the data contract

4. Browser

<scriptsrc="https://cdn.jsdelivr.net/npm/mailc/dist/browser.global.js"></script><script>const{ html }=mailc.compile(source)</script>

Core capabilities

Compile-time renderingNo runtime JS — pure static HTML output
Email-safe HTMLTable-based layouts, inline styles, Outlook VML, Gmail clip checks
Browser + NodeSame API everywhere — CDN, bundler, or CLI
Component systemmc-section, mc-column, mc-text, mc-button, mc-image, mc-hero, mc-list, …
Two styling modesAttribute-based (color="#0066cc") by default; opt-in Tailwind class mode for utility-first teams
Theming & design tokens12 extendable token scales — colors, spacing, fonts, radii, etc.
Templating{{variable}}, mc-if, mc-each, sandboxed (no eval, no Function)
caniemail integrationPer-property compatibility checks against your declared targetClients
Source mapsEvery output element traces back to its source node — bidirectional
AI-friendlyStructured FixInstruction errors, JSON IR, and an MCP server for AI agents
Plugin APIdefineComponent({ type, metadata, compile }) returns a Plugin value — pass to compile(src, { plugins }) or createCompiler({ plugins }). Stateless, multi-instance, no globals.

Two styling modes

mailc supports two styling paradigms. Pick what fits your team — or mix.

Attribute mode (default)

Familiar HTML-style API. CSS-property attributes go directly on components.

<mc-textcolor="#333" font-size="16px" padding="12px 0">
Hello world
</mc-text>

Class mode (limited support)

Tailwind-style utilities for teams that prefer utility-first. CSS-property attributes are flagged as warnings — you express styling via class="" instead.

<mc-textclass="text-[#333] text-[16px] py-3">
Hello world
</mc-text>
compile(source,{templateStyle: 'class'})

Some attributes (e.g. border shorthands, inner-background-color) have no Tailwind equivalent today and remain attribute-only even in class mode.


Custom components (plugins)

defineComponent() returns a plain Plugin value. Pass it to compile() per call, or bind it once with createCompiler(). There is no global registration — plugins are values.

import{defineComponent,compile,createCompiler}from'mailc';constproductCard=defineComponent({type: 'acme-product-card',// must contain `-`, must not start with `mc-`metadata: {description: 'Product card with image, title, and CTA.',category: 'content',parent: 'mc-column',maxChildren: 0,allowsTextContent: false,compilerOutputElements: ['table','tr','td'],compilerOutputReason: 'Centered, email-safe table.',validClassCategories: [],commonMistakes: [],attributes: {title: {type: 'string',required: true,description: 'Product title.',example: 'Acme Widget',hasEmailCompatibilityNotes: false},price: {type: 'string',required: true,description: 'Display price.',example: '$19.99',hasEmailCompatibilityNotes: false},},},compile: (node)=>{consttitle=node.attributes['title']??'';constprice=node.attributes['price']??'';return`<table><tr><td>${title}</td><td>${price}</td></tr></table>`;},});// Per-call:compile(source,{plugins: [productCard]});// Bind once for many calls:constmailc=createCompiler({plugins: [productCard]});mailc.compile(welcome);mailc.compile(receipt);// Multi-instance (different tenants, isolated plugins, same process):consttenantA=createCompiler({plugins: [productCard,tenantAExtras]});consttenantB=createCompiler({plugins: [tenantBExtras]});

The compile function emits HTML as-is — plugins are responsible for escaping user-controlled strings (escapeHtml is exported as a helper) and for any class-mode enforcement (assertClassModeAttributes is exported too). See examples/plugin-product-card for a worked example.


MCP server

mailc ships an MCP server so AI agents (Claude Desktop, Cursor, etc.) can author and validate emails with structured tools instead of guessing at email markup.

// ~/.cursor/mcp.json or ~/.claude/claude_desktop_config.json
{
"mcpServers": {
"mailc": {
"command": "npx",
"args": ["-y", "mailc-mcp"]
}
}
}

Exposes 7 tools: compile_email, validate_email_node, list_components, get_component_spec, can_nest, extract_data_contract, check_email_client_support.

Once configured, ask your agent "build me a welcome email using mailc" and it'll use the tools to compile, validate, and self-correct using structured FixInstruction errors.


Built for tools, not just for templates

mailc was designed so other tools can build on top of it. If you're building an email builder, design system, or AI integration:

  • JSON IRcompileFromJSON() skips the markup parser entirely. Builder state is the input.
  • Source maps — every output HTML element maps back to its source node with byte-precise ranges.
  • Introspection APIintrospect.canNest(), introspect.componentSpec(), introspect.dataContract(), and 8 more functions. Query the compiler instead of reading the docs.
  • Structured errorsFixInstruction objects with action codes (add-attribute, wrap-in, replace-with-class, …) so a UI can render auto-fix buttons.
  • Native dynamic templating{{variable}}, mc-if, mc-each are part of the compiler, not a separate layer. Pass data to compile() and the contract is statically derivable via extract_data_contract.
  • Tailwind-style class mode — opt in with templateStyle: 'class' and write class="text-[#333] py-3" instead of attributes. Same compiler, same output guarantees.
  • caniemail validation — declare targetClients and the compiler flags properties that won't render in your audience's clients, with per-property compatibility data sourced from caniemail.

Try it: the playground's introspection demo shows all of these live.


Playground

A full interactive playground with builder, theme studio, dynamic-email previews, marketplace plugins, and an introspection sandbox.

git clone https://github.com/amareshsm/mailc
cd mailc/playground
pnpm install && pnpm dev

Documentation

Full docs live at the mailc site — getting started, components reference, CLI/API, theming, templating, accessibility, JSON IR, and plugin authoring.


Contributing

Contributions, issues, and feedback are very welcome. Particularly useful at this stage:

  • Try it on a real email and tell me what broke. That's the highest-signal feedback.
  • Tell me what's confusing in the docs. I'm too close to it.
  • Open an issue if a component doesn't compile the way you expect. The CSS-stripping decisions are deliberate but sometimes wrong.
git clone https://github.com/amareshsm/mailc
cd mailc
pnpm install
pnpm test

Credits

mailc is heavily inspired by MJML. Several proven layout decisions were borrowed directly. The snapshot test suite also uses MJML as the reference implementation that mailc's output is checked against to catch regressions.


License

MIT


mailc is early-stage. The compiler works, but the ecosystem around it is still forming. If you'd like to help shape where it goes, the most useful thing you can do is try it on something real and share what you learn.

About

mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - amareshsm/mailc: mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating. · GitHub
Skip to content

Repository files navigation

mailc

A modern email markup compiler — attribute or Tailwind styling, email-safe, developer-first with native dynamic templating.

Write emails as components. Compile to HTML that works in Gmail, Outlook, Apple Mail, and the rest of the long tail. Use Tailwind classes if you want; use plain CSS attributes if you don't.

<mc><mc-body><mc-sectionpadding="32px 0"><mc-column><mc-textfont-size="24px" font-weight="bold" align="center">
Welcome aboard!
</mc-text><mc-buttonhref="https://example.com" background-color="#0066cc" color="#ffffff">
Get Started
</mc-button></mc-column></mc-section></mc-body></mc>
import{compile}from'mailc'const{ html, errors, warnings }=compile(source)// html is ready to send. errors/warnings tell you what's broken.

That's the whole API for the common case.

Status: early. The compiler is solid (3,700+ tests, all passing) but the user base is small. If you try it and hit something rough, please open an issue — feedback at this stage is the most useful contribution you can make.


Why mailc

Email HTML is awful. You can't use flexbox, grid, or modern CSS. You need tables nested inside tables. Outlook needs separate VML markup for backgrounds. Gmail strips <style> blocks under certain conditions. mailc handles all of that and gives you a clean component model on top.

You'd reach for mailc when:

  • You want a TypeScript-native compiler with structured errors, not a black-box CLI
  • You want to use Tailwind utilities, attribute-based styling, or both
  • You want to build a tool on top of an email compiler — JSON IR, source maps, plugin API, AI-agent integration are first-class

Install

npm install mailc
# or
pnpm add mailc
# or
yarn add mailc

Requires Node 20+ for the CLI / Node API. The browser bundle works in any modern browser.


Quick start

1. Compile from a string

import{compile}from'mailc'constresult=compile(` <mc> <mc-body> <mc-section> <mc-column> <mc-text>Hello {{user.name}}</mc-text> </mc-column> </mc-section> </mc-body> </mc>`,{data: {user: {name: 'Ada'}},})console.log(result.html)// production-ready HTMLconsole.log(result.errors)// [] when cleanconsole.log(result.warnings)// non-blocking issues

2. Compile from JSON (for builder UIs)

import{compileFromJSON}from'mailc'constresult=compileFromJSON({type: 'mc',children: [{type: 'mc-body',children: [/* ... */],},],})

3. CLI

mailc build welcome.mc -o welcome.html
mailc validate welcome.mc
mailc watch welcome.mc -o welcome.html --serve # live-reload preview server
mailc init # scaffold a new project
mailc contract welcome.mc # show the data contract

4. Browser

<scriptsrc="https://cdn.jsdelivr.net/npm/mailc/dist/browser.global.js"></script><script>const{ html }=mailc.compile(source)</script>

Core capabilities

Compile-time renderingNo runtime JS — pure static HTML output
Email-safe HTMLTable-based layouts, inline styles, Outlook VML, Gmail clip checks
Browser + NodeSame API everywhere — CDN, bundler, or CLI
Component systemmc-section, mc-column, mc-text, mc-button, mc-image, mc-hero, mc-list, …
Two styling modesAttribute-based (color="#0066cc") by default; opt-in Tailwind class mode for utility-first teams
Theming & design tokens12 extendable token scales — colors, spacing, fonts, radii, etc.
Templating{{variable}}, mc-if, mc-each, sandboxed (no eval, no Function)
caniemail integrationPer-property compatibility checks against your declared targetClients
Source mapsEvery output element traces back to its source node — bidirectional
AI-friendlyStructured FixInstruction errors, JSON IR, and an MCP server for AI agents
Plugin APIdefineComponent({ type, metadata, compile }) returns a Plugin value — pass to compile(src, { plugins }) or createCompiler({ plugins }). Stateless, multi-instance, no globals.

Two styling modes

mailc supports two styling paradigms. Pick what fits your team — or mix.

Attribute mode (default)

Familiar HTML-style API. CSS-property attributes go directly on components.

<mc-textcolor="#333" font-size="16px" padding="12px 0">
Hello world
</mc-text>

Class mode (limited support)

Tailwind-style utilities for teams that prefer utility-first. CSS-property attributes are flagged as warnings — you express styling via class="" instead.

<mc-textclass="text-[#333] text-[16px] py-3">
Hello world
</mc-text>
compile(source,{templateStyle: 'class'})

Some attributes (e.g. border shorthands, inner-background-color) have no Tailwind equivalent today and remain attribute-only even in class mode.


Custom components (plugins)

defineComponent() returns a plain Plugin value. Pass it to compile() per call, or bind it once with createCompiler(). There is no global registration — plugins are values.

import{defineComponent,compile,createCompiler}from'mailc';constproductCard=defineComponent({type: 'acme-product-card',// must contain `-`, must not start with `mc-`metadata: {description: 'Product card with image, title, and CTA.',category: 'content',parent: 'mc-column',maxChildren: 0,allowsTextContent: false,compilerOutputElements: ['table','tr','td'],compilerOutputReason: 'Centered, email-safe table.',validClassCategories: [],commonMistakes: [],attributes: {title: {type: 'string',required: true,description: 'Product title.',example: 'Acme Widget',hasEmailCompatibilityNotes: false},price: {type: 'string',required: true,description: 'Display price.',example: '$19.99',hasEmailCompatibilityNotes: false},},},compile: (node)=>{consttitle=node.attributes['title']??'';constprice=node.attributes['price']??'';return`<table><tr><td>${title}</td><td>${price}</td></tr></table>`;},});// Per-call:compile(source,{plugins: [productCard]});// Bind once for many calls:constmailc=createCompiler({plugins: [productCard]});mailc.compile(welcome);mailc.compile(receipt);// Multi-instance (different tenants, isolated plugins, same process):consttenantA=createCompiler({plugins: [productCard,tenantAExtras]});consttenantB=createCompiler({plugins: [tenantBExtras]});

The compile function emits HTML as-is — plugins are responsible for escaping user-controlled strings (escapeHtml is exported as a helper) and for any class-mode enforcement (assertClassModeAttributes is exported too). See examples/plugin-product-card for a worked example.


MCP server

mailc ships an MCP server so AI agents (Claude Desktop, Cursor, etc.) can author and validate emails with structured tools instead of guessing at email markup.

// ~/.cursor/mcp.json or ~/.claude/claude_desktop_config.json
{
"mcpServers": {
"mailc": {
"command": "npx",
"args": ["-y", "mailc-mcp"]
}
}
}

Exposes 7 tools: compile_email, validate_email_node, list_components, get_component_spec, can_nest, extract_data_contract, check_email_client_support.

Once configured, ask your agent "build me a welcome email using mailc" and it'll use the tools to compile, validate, and self-correct using structured FixInstruction errors.


Built for tools, not just for templates

mailc was designed so other tools can build on top of it. If you're building an email builder, design system, or AI integration:

  • JSON IRcompileFromJSON() skips the markup parser entirely. Builder state is the input.
  • Source maps — every output HTML element maps back to its source node with byte-precise ranges.
  • Introspection APIintrospect.canNest(), introspect.componentSpec(), introspect.dataContract(), and 8 more functions. Query the compiler instead of reading the docs.
  • Structured errorsFixInstruction objects with action codes (add-attribute, wrap-in, replace-with-class, …) so a UI can render auto-fix buttons.
  • Native dynamic templating{{variable}}, mc-if, mc-each are part of the compiler, not a separate layer. Pass data to compile() and the contract is statically derivable via extract_data_contract.
  • Tailwind-style class mode — opt in with templateStyle: 'class' and write class="text-[#333] py-3" instead of attributes. Same compiler, same output guarantees.
  • caniemail validation — declare targetClients and the compiler flags properties that won't render in your audience's clients, with per-property compatibility data sourced from caniemail.

Try it: the playground's introspection demo shows all of these live.


Playground

A full interactive playground with builder, theme studio, dynamic-email previews, marketplace plugins, and an introspection sandbox.

git clone https://github.com/amareshsm/mailc
cd mailc/playground
pnpm install && pnpm dev

Documentation

Full docs live at the mailc site — getting started, components reference, CLI/API, theming, templating, accessibility, JSON IR, and plugin authoring.


Contributing

Contributions, issues, and feedback are very welcome. Particularly useful at this stage:

  • Try it on a real email and tell me what broke. That's the highest-signal feedback.
  • Tell me what's confusing in the docs. I'm too close to it.
  • Open an issue if a component doesn't compile the way you expect. The CSS-stripping decisions are deliberate but sometimes wrong.
git clone https://github.com/amareshsm/mailc
cd mailc
pnpm install
pnpm test

Credits

mailc is heavily inspired by MJML. Several proven layout decisions were borrowed directly. The snapshot test suite also uses MJML as the reference implementation that mailc's output is checked against to catch regressions.


License

MIT


mailc is early-stage. The compiler works, but the ecosystem around it is still forming. If you'd like to help shape where it goes, the most useful thing you can do is try it on something real and share what you learn.

About

mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - amareshsm/mailc: mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating. · GitHub
Skip to content

Repository files navigation

mailc

A modern email markup compiler — attribute or Tailwind styling, email-safe, developer-first with native dynamic templating.

Write emails as components. Compile to HTML that works in Gmail, Outlook, Apple Mail, and the rest of the long tail. Use Tailwind classes if you want; use plain CSS attributes if you don't.

<mc><mc-body><mc-sectionpadding="32px 0"><mc-column><mc-textfont-size="24px" font-weight="bold" align="center">
Welcome aboard!
</mc-text><mc-buttonhref="https://example.com" background-color="#0066cc" color="#ffffff">
Get Started
</mc-button></mc-column></mc-section></mc-body></mc>
import{compile}from'mailc'const{ html, errors, warnings }=compile(source)// html is ready to send. errors/warnings tell you what's broken.

That's the whole API for the common case.

Status: early. The compiler is solid (3,700+ tests, all passing) but the user base is small. If you try it and hit something rough, please open an issue — feedback at this stage is the most useful contribution you can make.


Why mailc

Email HTML is awful. You can't use flexbox, grid, or modern CSS. You need tables nested inside tables. Outlook needs separate VML markup for backgrounds. Gmail strips <style> blocks under certain conditions. mailc handles all of that and gives you a clean component model on top.

You'd reach for mailc when:

  • You want a TypeScript-native compiler with structured errors, not a black-box CLI
  • You want to use Tailwind utilities, attribute-based styling, or both
  • You want to build a tool on top of an email compiler — JSON IR, source maps, plugin API, AI-agent integration are first-class

Install

npm install mailc
# or
pnpm add mailc
# or
yarn add mailc

Requires Node 20+ for the CLI / Node API. The browser bundle works in any modern browser.


Quick start

1. Compile from a string

import{compile}from'mailc'constresult=compile(` <mc> <mc-body> <mc-section> <mc-column> <mc-text>Hello {{user.name}}</mc-text> </mc-column> </mc-section> </mc-body> </mc>`,{data: {user: {name: 'Ada'}},})console.log(result.html)// production-ready HTMLconsole.log(result.errors)// [] when cleanconsole.log(result.warnings)// non-blocking issues

2. Compile from JSON (for builder UIs)

import{compileFromJSON}from'mailc'constresult=compileFromJSON({type: 'mc',children: [{type: 'mc-body',children: [/* ... */],},],})

3. CLI

mailc build welcome.mc -o welcome.html
mailc validate welcome.mc
mailc watch welcome.mc -o welcome.html --serve # live-reload preview server
mailc init # scaffold a new project
mailc contract welcome.mc # show the data contract

4. Browser

<scriptsrc="https://cdn.jsdelivr.net/npm/mailc/dist/browser.global.js"></script><script>const{ html }=mailc.compile(source)</script>

Core capabilities

Compile-time renderingNo runtime JS — pure static HTML output
Email-safe HTMLTable-based layouts, inline styles, Outlook VML, Gmail clip checks
Browser + NodeSame API everywhere — CDN, bundler, or CLI
Component systemmc-section, mc-column, mc-text, mc-button, mc-image, mc-hero, mc-list, …
Two styling modesAttribute-based (color="#0066cc") by default; opt-in Tailwind class mode for utility-first teams
Theming & design tokens12 extendable token scales — colors, spacing, fonts, radii, etc.
Templating{{variable}}, mc-if, mc-each, sandboxed (no eval, no Function)
caniemail integrationPer-property compatibility checks against your declared targetClients
Source mapsEvery output element traces back to its source node — bidirectional
AI-friendlyStructured FixInstruction errors, JSON IR, and an MCP server for AI agents
Plugin APIdefineComponent({ type, metadata, compile }) returns a Plugin value — pass to compile(src, { plugins }) or createCompiler({ plugins }). Stateless, multi-instance, no globals.

Two styling modes

mailc supports two styling paradigms. Pick what fits your team — or mix.

Attribute mode (default)

Familiar HTML-style API. CSS-property attributes go directly on components.

<mc-textcolor="#333" font-size="16px" padding="12px 0">
Hello world
</mc-text>

Class mode (limited support)

Tailwind-style utilities for teams that prefer utility-first. CSS-property attributes are flagged as warnings — you express styling via class="" instead.

<mc-textclass="text-[#333] text-[16px] py-3">
Hello world
</mc-text>
compile(source,{templateStyle: 'class'})

Some attributes (e.g. border shorthands, inner-background-color) have no Tailwind equivalent today and remain attribute-only even in class mode.


Custom components (plugins)

defineComponent() returns a plain Plugin value. Pass it to compile() per call, or bind it once with createCompiler(). There is no global registration — plugins are values.

import{defineComponent,compile,createCompiler}from'mailc';constproductCard=defineComponent({type: 'acme-product-card',// must contain `-`, must not start with `mc-`metadata: {description: 'Product card with image, title, and CTA.',category: 'content',parent: 'mc-column',maxChildren: 0,allowsTextContent: false,compilerOutputElements: ['table','tr','td'],compilerOutputReason: 'Centered, email-safe table.',validClassCategories: [],commonMistakes: [],attributes: {title: {type: 'string',required: true,description: 'Product title.',example: 'Acme Widget',hasEmailCompatibilityNotes: false},price: {type: 'string',required: true,description: 'Display price.',example: '$19.99',hasEmailCompatibilityNotes: false},},},compile: (node)=>{consttitle=node.attributes['title']??'';constprice=node.attributes['price']??'';return`<table><tr><td>${title}</td><td>${price}</td></tr></table>`;},});// Per-call:compile(source,{plugins: [productCard]});// Bind once for many calls:constmailc=createCompiler({plugins: [productCard]});mailc.compile(welcome);mailc.compile(receipt);// Multi-instance (different tenants, isolated plugins, same process):consttenantA=createCompiler({plugins: [productCard,tenantAExtras]});consttenantB=createCompiler({plugins: [tenantBExtras]});

The compile function emits HTML as-is — plugins are responsible for escaping user-controlled strings (escapeHtml is exported as a helper) and for any class-mode enforcement (assertClassModeAttributes is exported too). See examples/plugin-product-card for a worked example.


MCP server

mailc ships an MCP server so AI agents (Claude Desktop, Cursor, etc.) can author and validate emails with structured tools instead of guessing at email markup.

// ~/.cursor/mcp.json or ~/.claude/claude_desktop_config.json
{
"mcpServers": {
"mailc": {
"command": "npx",
"args": ["-y", "mailc-mcp"]
}
}
}

Exposes 7 tools: compile_email, validate_email_node, list_components, get_component_spec, can_nest, extract_data_contract, check_email_client_support.

Once configured, ask your agent "build me a welcome email using mailc" and it'll use the tools to compile, validate, and self-correct using structured FixInstruction errors.


Built for tools, not just for templates

mailc was designed so other tools can build on top of it. If you're building an email builder, design system, or AI integration:

  • JSON IRcompileFromJSON() skips the markup parser entirely. Builder state is the input.
  • Source maps — every output HTML element maps back to its source node with byte-precise ranges.
  • Introspection APIintrospect.canNest(), introspect.componentSpec(), introspect.dataContract(), and 8 more functions. Query the compiler instead of reading the docs.
  • Structured errorsFixInstruction objects with action codes (add-attribute, wrap-in, replace-with-class, …) so a UI can render auto-fix buttons.
  • Native dynamic templating{{variable}}, mc-if, mc-each are part of the compiler, not a separate layer. Pass data to compile() and the contract is statically derivable via extract_data_contract.
  • Tailwind-style class mode — opt in with templateStyle: 'class' and write class="text-[#333] py-3" instead of attributes. Same compiler, same output guarantees.
  • caniemail validation — declare targetClients and the compiler flags properties that won't render in your audience's clients, with per-property compatibility data sourced from caniemail.

Try it: the playground's introspection demo shows all of these live.


Playground

A full interactive playground with builder, theme studio, dynamic-email previews, marketplace plugins, and an introspection sandbox.

git clone https://github.com/amareshsm/mailc
cd mailc/playground
pnpm install && pnpm dev

Documentation

Full docs live at the mailc site — getting started, components reference, CLI/API, theming, templating, accessibility, JSON IR, and plugin authoring.


Contributing

Contributions, issues, and feedback are very welcome. Particularly useful at this stage:

  • Try it on a real email and tell me what broke. That's the highest-signal feedback.
  • Tell me what's confusing in the docs. I'm too close to it.
  • Open an issue if a component doesn't compile the way you expect. The CSS-stripping decisions are deliberate but sometimes wrong.
git clone https://github.com/amareshsm/mailc
cd mailc
pnpm install
pnpm test

Credits

mailc is heavily inspired by MJML. Several proven layout decisions were borrowed directly. The snapshot test suite also uses MJML as the reference implementation that mailc's output is checked against to catch regressions.


License

MIT


mailc is early-stage. The compiler works, but the ecosystem around it is still forming. If you'd like to help shape where it goes, the most useful thing you can do is try it on something real and share what you learn.

About

mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - amareshsm/mailc: mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating. · GitHub
Skip to content

Repository files navigation

mailc

A modern email markup compiler — attribute or Tailwind styling, email-safe, developer-first with native dynamic templating.

Write emails as components. Compile to HTML that works in Gmail, Outlook, Apple Mail, and the rest of the long tail. Use Tailwind classes if you want; use plain CSS attributes if you don't.

<mc><mc-body><mc-sectionpadding="32px 0"><mc-column><mc-textfont-size="24px" font-weight="bold" align="center">
Welcome aboard!
</mc-text><mc-buttonhref="https://example.com" background-color="#0066cc" color="#ffffff">
Get Started
</mc-button></mc-column></mc-section></mc-body></mc>
import{compile}from'mailc'const{ html, errors, warnings }=compile(source)// html is ready to send. errors/warnings tell you what's broken.

That's the whole API for the common case.

Status: early. The compiler is solid (3,700+ tests, all passing) but the user base is small. If you try it and hit something rough, please open an issue — feedback at this stage is the most useful contribution you can make.


Why mailc

Email HTML is awful. You can't use flexbox, grid, or modern CSS. You need tables nested inside tables. Outlook needs separate VML markup for backgrounds. Gmail strips <style> blocks under certain conditions. mailc handles all of that and gives you a clean component model on top.

You'd reach for mailc when:

  • You want a TypeScript-native compiler with structured errors, not a black-box CLI
  • You want to use Tailwind utilities, attribute-based styling, or both
  • You want to build a tool on top of an email compiler — JSON IR, source maps, plugin API, AI-agent integration are first-class

Install

npm install mailc
# or
pnpm add mailc
# or
yarn add mailc

Requires Node 20+ for the CLI / Node API. The browser bundle works in any modern browser.


Quick start

1. Compile from a string

import{compile}from'mailc'constresult=compile(` <mc> <mc-body> <mc-section> <mc-column> <mc-text>Hello {{user.name}}</mc-text> </mc-column> </mc-section> </mc-body> </mc>`,{data: {user: {name: 'Ada'}},})console.log(result.html)// production-ready HTMLconsole.log(result.errors)// [] when cleanconsole.log(result.warnings)// non-blocking issues

2. Compile from JSON (for builder UIs)

import{compileFromJSON}from'mailc'constresult=compileFromJSON({type: 'mc',children: [{type: 'mc-body',children: [/* ... */],},],})

3. CLI

mailc build welcome.mc -o welcome.html
mailc validate welcome.mc
mailc watch welcome.mc -o welcome.html --serve # live-reload preview server
mailc init # scaffold a new project
mailc contract welcome.mc # show the data contract

4. Browser

<scriptsrc="https://cdn.jsdelivr.net/npm/mailc/dist/browser.global.js"></script><script>const{ html }=mailc.compile(source)</script>

Core capabilities

Compile-time renderingNo runtime JS — pure static HTML output
Email-safe HTMLTable-based layouts, inline styles, Outlook VML, Gmail clip checks
Browser + NodeSame API everywhere — CDN, bundler, or CLI
Component systemmc-section, mc-column, mc-text, mc-button, mc-image, mc-hero, mc-list, …
Two styling modesAttribute-based (color="#0066cc") by default; opt-in Tailwind class mode for utility-first teams
Theming & design tokens12 extendable token scales — colors, spacing, fonts, radii, etc.
Templating{{variable}}, mc-if, mc-each, sandboxed (no eval, no Function)
caniemail integrationPer-property compatibility checks against your declared targetClients
Source mapsEvery output element traces back to its source node — bidirectional
AI-friendlyStructured FixInstruction errors, JSON IR, and an MCP server for AI agents
Plugin APIdefineComponent({ type, metadata, compile }) returns a Plugin value — pass to compile(src, { plugins }) or createCompiler({ plugins }). Stateless, multi-instance, no globals.

Two styling modes

mailc supports two styling paradigms. Pick what fits your team — or mix.

Attribute mode (default)

Familiar HTML-style API. CSS-property attributes go directly on components.

<mc-textcolor="#333" font-size="16px" padding="12px 0">
Hello world
</mc-text>

Class mode (limited support)

Tailwind-style utilities for teams that prefer utility-first. CSS-property attributes are flagged as warnings — you express styling via class="" instead.

<mc-textclass="text-[#333] text-[16px] py-3">
Hello world
</mc-text>
compile(source,{templateStyle: 'class'})

Some attributes (e.g. border shorthands, inner-background-color) have no Tailwind equivalent today and remain attribute-only even in class mode.


Custom components (plugins)

defineComponent() returns a plain Plugin value. Pass it to compile() per call, or bind it once with createCompiler(). There is no global registration — plugins are values.

import{defineComponent,compile,createCompiler}from'mailc';constproductCard=defineComponent({type: 'acme-product-card',// must contain `-`, must not start with `mc-`metadata: {description: 'Product card with image, title, and CTA.',category: 'content',parent: 'mc-column',maxChildren: 0,allowsTextContent: false,compilerOutputElements: ['table','tr','td'],compilerOutputReason: 'Centered, email-safe table.',validClassCategories: [],commonMistakes: [],attributes: {title: {type: 'string',required: true,description: 'Product title.',example: 'Acme Widget',hasEmailCompatibilityNotes: false},price: {type: 'string',required: true,description: 'Display price.',example: '$19.99',hasEmailCompatibilityNotes: false},},},compile: (node)=>{consttitle=node.attributes['title']??'';constprice=node.attributes['price']??'';return`<table><tr><td>${title}</td><td>${price}</td></tr></table>`;},});// Per-call:compile(source,{plugins: [productCard]});// Bind once for many calls:constmailc=createCompiler({plugins: [productCard]});mailc.compile(welcome);mailc.compile(receipt);// Multi-instance (different tenants, isolated plugins, same process):consttenantA=createCompiler({plugins: [productCard,tenantAExtras]});consttenantB=createCompiler({plugins: [tenantBExtras]});

The compile function emits HTML as-is — plugins are responsible for escaping user-controlled strings (escapeHtml is exported as a helper) and for any class-mode enforcement (assertClassModeAttributes is exported too). See examples/plugin-product-card for a worked example.


MCP server

mailc ships an MCP server so AI agents (Claude Desktop, Cursor, etc.) can author and validate emails with structured tools instead of guessing at email markup.

// ~/.cursor/mcp.json or ~/.claude/claude_desktop_config.json
{
"mcpServers": {
"mailc": {
"command": "npx",
"args": ["-y", "mailc-mcp"]
}
}
}

Exposes 7 tools: compile_email, validate_email_node, list_components, get_component_spec, can_nest, extract_data_contract, check_email_client_support.

Once configured, ask your agent "build me a welcome email using mailc" and it'll use the tools to compile, validate, and self-correct using structured FixInstruction errors.


Built for tools, not just for templates

mailc was designed so other tools can build on top of it. If you're building an email builder, design system, or AI integration:

  • JSON IRcompileFromJSON() skips the markup parser entirely. Builder state is the input.
  • Source maps — every output HTML element maps back to its source node with byte-precise ranges.
  • Introspection APIintrospect.canNest(), introspect.componentSpec(), introspect.dataContract(), and 8 more functions. Query the compiler instead of reading the docs.
  • Structured errorsFixInstruction objects with action codes (add-attribute, wrap-in, replace-with-class, …) so a UI can render auto-fix buttons.
  • Native dynamic templating{{variable}}, mc-if, mc-each are part of the compiler, not a separate layer. Pass data to compile() and the contract is statically derivable via extract_data_contract.
  • Tailwind-style class mode — opt in with templateStyle: 'class' and write class="text-[#333] py-3" instead of attributes. Same compiler, same output guarantees.
  • caniemail validation — declare targetClients and the compiler flags properties that won't render in your audience's clients, with per-property compatibility data sourced from caniemail.

Try it: the playground's introspection demo shows all of these live.


Playground

A full interactive playground with builder, theme studio, dynamic-email previews, marketplace plugins, and an introspection sandbox.

git clone https://github.com/amareshsm/mailc
cd mailc/playground
pnpm install && pnpm dev

Documentation

Full docs live at the mailc site — getting started, components reference, CLI/API, theming, templating, accessibility, JSON IR, and plugin authoring.


Contributing

Contributions, issues, and feedback are very welcome. Particularly useful at this stage:

  • Try it on a real email and tell me what broke. That's the highest-signal feedback.
  • Tell me what's confusing in the docs. I'm too close to it.
  • Open an issue if a component doesn't compile the way you expect. The CSS-stripping decisions are deliberate but sometimes wrong.
git clone https://github.com/amareshsm/mailc
cd mailc
pnpm install
pnpm test

Credits

mailc is heavily inspired by MJML. Several proven layout decisions were borrowed directly. The snapshot test suite also uses MJML as the reference implementation that mailc's output is checked against to catch regressions.


License

MIT


mailc is early-stage. The compiler works, but the ecosystem around it is still forming. If you'd like to help shape where it goes, the most useful thing you can do is try it on something real and share what you learn.

About

mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - amareshsm/mailc: mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating. · GitHub
Skip to content

Repository files navigation

mailc

A modern email markup compiler — attribute or Tailwind styling, email-safe, developer-first with native dynamic templating.

Write emails as components. Compile to HTML that works in Gmail, Outlook, Apple Mail, and the rest of the long tail. Use Tailwind classes if you want; use plain CSS attributes if you don't.

<mc><mc-body><mc-sectionpadding="32px 0"><mc-column><mc-textfont-size="24px" font-weight="bold" align="center">
Welcome aboard!
</mc-text><mc-buttonhref="https://example.com" background-color="#0066cc" color="#ffffff">
Get Started
</mc-button></mc-column></mc-section></mc-body></mc>
import{compile}from'mailc'const{ html, errors, warnings }=compile(source)// html is ready to send. errors/warnings tell you what's broken.

That's the whole API for the common case.

Status: early. The compiler is solid (3,700+ tests, all passing) but the user base is small. If you try it and hit something rough, please open an issue — feedback at this stage is the most useful contribution you can make.


Why mailc

Email HTML is awful. You can't use flexbox, grid, or modern CSS. You need tables nested inside tables. Outlook needs separate VML markup for backgrounds. Gmail strips <style> blocks under certain conditions. mailc handles all of that and gives you a clean component model on top.

You'd reach for mailc when:

  • You want a TypeScript-native compiler with structured errors, not a black-box CLI
  • You want to use Tailwind utilities, attribute-based styling, or both
  • You want to build a tool on top of an email compiler — JSON IR, source maps, plugin API, AI-agent integration are first-class

Install

npm install mailc
# or
pnpm add mailc
# or
yarn add mailc

Requires Node 20+ for the CLI / Node API. The browser bundle works in any modern browser.


Quick start

1. Compile from a string

import{compile}from'mailc'constresult=compile(` <mc> <mc-body> <mc-section> <mc-column> <mc-text>Hello {{user.name}}</mc-text> </mc-column> </mc-section> </mc-body> </mc>`,{data: {user: {name: 'Ada'}},})console.log(result.html)// production-ready HTMLconsole.log(result.errors)// [] when cleanconsole.log(result.warnings)// non-blocking issues

2. Compile from JSON (for builder UIs)

import{compileFromJSON}from'mailc'constresult=compileFromJSON({type: 'mc',children: [{type: 'mc-body',children: [/* ... */],},],})

3. CLI

mailc build welcome.mc -o welcome.html
mailc validate welcome.mc
mailc watch welcome.mc -o welcome.html --serve # live-reload preview server
mailc init # scaffold a new project
mailc contract welcome.mc # show the data contract

4. Browser

<scriptsrc="https://cdn.jsdelivr.net/npm/mailc/dist/browser.global.js"></script><script>const{ html }=mailc.compile(source)</script>

Core capabilities

Compile-time renderingNo runtime JS — pure static HTML output
Email-safe HTMLTable-based layouts, inline styles, Outlook VML, Gmail clip checks
Browser + NodeSame API everywhere — CDN, bundler, or CLI
Component systemmc-section, mc-column, mc-text, mc-button, mc-image, mc-hero, mc-list, …
Two styling modesAttribute-based (color="#0066cc") by default; opt-in Tailwind class mode for utility-first teams
Theming & design tokens12 extendable token scales — colors, spacing, fonts, radii, etc.
Templating{{variable}}, mc-if, mc-each, sandboxed (no eval, no Function)
caniemail integrationPer-property compatibility checks against your declared targetClients
Source mapsEvery output element traces back to its source node — bidirectional
AI-friendlyStructured FixInstruction errors, JSON IR, and an MCP server for AI agents
Plugin APIdefineComponent({ type, metadata, compile }) returns a Plugin value — pass to compile(src, { plugins }) or createCompiler({ plugins }). Stateless, multi-instance, no globals.

Two styling modes

mailc supports two styling paradigms. Pick what fits your team — or mix.

Attribute mode (default)

Familiar HTML-style API. CSS-property attributes go directly on components.

<mc-textcolor="#333" font-size="16px" padding="12px 0">
Hello world
</mc-text>

Class mode (limited support)

Tailwind-style utilities for teams that prefer utility-first. CSS-property attributes are flagged as warnings — you express styling via class="" instead.

<mc-textclass="text-[#333] text-[16px] py-3">
Hello world
</mc-text>
compile(source,{templateStyle: 'class'})

Some attributes (e.g. border shorthands, inner-background-color) have no Tailwind equivalent today and remain attribute-only even in class mode.


Custom components (plugins)

defineComponent() returns a plain Plugin value. Pass it to compile() per call, or bind it once with createCompiler(). There is no global registration — plugins are values.

import{defineComponent,compile,createCompiler}from'mailc';constproductCard=defineComponent({type: 'acme-product-card',// must contain `-`, must not start with `mc-`metadata: {description: 'Product card with image, title, and CTA.',category: 'content',parent: 'mc-column',maxChildren: 0,allowsTextContent: false,compilerOutputElements: ['table','tr','td'],compilerOutputReason: 'Centered, email-safe table.',validClassCategories: [],commonMistakes: [],attributes: {title: {type: 'string',required: true,description: 'Product title.',example: 'Acme Widget',hasEmailCompatibilityNotes: false},price: {type: 'string',required: true,description: 'Display price.',example: '$19.99',hasEmailCompatibilityNotes: false},},},compile: (node)=>{consttitle=node.attributes['title']??'';constprice=node.attributes['price']??'';return`<table><tr><td>${title}</td><td>${price}</td></tr></table>`;},});// Per-call:compile(source,{plugins: [productCard]});// Bind once for many calls:constmailc=createCompiler({plugins: [productCard]});mailc.compile(welcome);mailc.compile(receipt);// Multi-instance (different tenants, isolated plugins, same process):consttenantA=createCompiler({plugins: [productCard,tenantAExtras]});consttenantB=createCompiler({plugins: [tenantBExtras]});

The compile function emits HTML as-is — plugins are responsible for escaping user-controlled strings (escapeHtml is exported as a helper) and for any class-mode enforcement (assertClassModeAttributes is exported too). See examples/plugin-product-card for a worked example.


MCP server

mailc ships an MCP server so AI agents (Claude Desktop, Cursor, etc.) can author and validate emails with structured tools instead of guessing at email markup.

// ~/.cursor/mcp.json or ~/.claude/claude_desktop_config.json
{
"mcpServers": {
"mailc": {
"command": "npx",
"args": ["-y", "mailc-mcp"]
}
}
}

Exposes 7 tools: compile_email, validate_email_node, list_components, get_component_spec, can_nest, extract_data_contract, check_email_client_support.

Once configured, ask your agent "build me a welcome email using mailc" and it'll use the tools to compile, validate, and self-correct using structured FixInstruction errors.


Built for tools, not just for templates

mailc was designed so other tools can build on top of it. If you're building an email builder, design system, or AI integration:

  • JSON IRcompileFromJSON() skips the markup parser entirely. Builder state is the input.
  • Source maps — every output HTML element maps back to its source node with byte-precise ranges.
  • Introspection APIintrospect.canNest(), introspect.componentSpec(), introspect.dataContract(), and 8 more functions. Query the compiler instead of reading the docs.
  • Structured errorsFixInstruction objects with action codes (add-attribute, wrap-in, replace-with-class, …) so a UI can render auto-fix buttons.
  • Native dynamic templating{{variable}}, mc-if, mc-each are part of the compiler, not a separate layer. Pass data to compile() and the contract is statically derivable via extract_data_contract.
  • Tailwind-style class mode — opt in with templateStyle: 'class' and write class="text-[#333] py-3" instead of attributes. Same compiler, same output guarantees.
  • caniemail validation — declare targetClients and the compiler flags properties that won't render in your audience's clients, with per-property compatibility data sourced from caniemail.

Try it: the playground's introspection demo shows all of these live.


Playground

A full interactive playground with builder, theme studio, dynamic-email previews, marketplace plugins, and an introspection sandbox.

git clone https://github.com/amareshsm/mailc
cd mailc/playground
pnpm install && pnpm dev

Documentation

Full docs live at the mailc site — getting started, components reference, CLI/API, theming, templating, accessibility, JSON IR, and plugin authoring.


Contributing

Contributions, issues, and feedback are very welcome. Particularly useful at this stage:

  • Try it on a real email and tell me what broke. That's the highest-signal feedback.
  • Tell me what's confusing in the docs. I'm too close to it.
  • Open an issue if a component doesn't compile the way you expect. The CSS-stripping decisions are deliberate but sometimes wrong.
git clone https://github.com/amareshsm/mailc
cd mailc
pnpm install
pnpm test

Credits

mailc is heavily inspired by MJML. Several proven layout decisions were borrowed directly. The snapshot test suite also uses MJML as the reference implementation that mailc's output is checked against to catch regressions.


License

MIT


mailc is early-stage. The compiler works, but the ecosystem around it is still forming. If you'd like to help shape where it goes, the most useful thing you can do is try it on something real and share what you learn.

About

mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - amareshsm/mailc: mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating. · GitHub
Skip to content

Repository files navigation

mailc

A modern email markup compiler — attribute or Tailwind styling, email-safe, developer-first with native dynamic templating.

Write emails as components. Compile to HTML that works in Gmail, Outlook, Apple Mail, and the rest of the long tail. Use Tailwind classes if you want; use plain CSS attributes if you don't.

<mc><mc-body><mc-sectionpadding="32px 0"><mc-column><mc-textfont-size="24px" font-weight="bold" align="center">
Welcome aboard!
</mc-text><mc-buttonhref="https://example.com" background-color="#0066cc" color="#ffffff">
Get Started
</mc-button></mc-column></mc-section></mc-body></mc>
import{compile}from'mailc'const{ html, errors, warnings }=compile(source)// html is ready to send. errors/warnings tell you what's broken.

That's the whole API for the common case.

Status: early. The compiler is solid (3,700+ tests, all passing) but the user base is small. If you try it and hit something rough, please open an issue — feedback at this stage is the most useful contribution you can make.


Why mailc

Email HTML is awful. You can't use flexbox, grid, or modern CSS. You need tables nested inside tables. Outlook needs separate VML markup for backgrounds. Gmail strips <style> blocks under certain conditions. mailc handles all of that and gives you a clean component model on top.

You'd reach for mailc when:

  • You want a TypeScript-native compiler with structured errors, not a black-box CLI
  • You want to use Tailwind utilities, attribute-based styling, or both
  • You want to build a tool on top of an email compiler — JSON IR, source maps, plugin API, AI-agent integration are first-class

Install

npm install mailc
# or
pnpm add mailc
# or
yarn add mailc

Requires Node 20+ for the CLI / Node API. The browser bundle works in any modern browser.


Quick start

1. Compile from a string

import{compile}from'mailc'constresult=compile(` <mc> <mc-body> <mc-section> <mc-column> <mc-text>Hello {{user.name}}</mc-text> </mc-column> </mc-section> </mc-body> </mc>`,{data: {user: {name: 'Ada'}},})console.log(result.html)// production-ready HTMLconsole.log(result.errors)// [] when cleanconsole.log(result.warnings)// non-blocking issues

2. Compile from JSON (for builder UIs)

import{compileFromJSON}from'mailc'constresult=compileFromJSON({type: 'mc',children: [{type: 'mc-body',children: [/* ... */],},],})

3. CLI

mailc build welcome.mc -o welcome.html
mailc validate welcome.mc
mailc watch welcome.mc -o welcome.html --serve # live-reload preview server
mailc init # scaffold a new project
mailc contract welcome.mc # show the data contract

4. Browser

<scriptsrc="https://cdn.jsdelivr.net/npm/mailc/dist/browser.global.js"></script><script>const{ html }=mailc.compile(source)</script>

Core capabilities

Compile-time renderingNo runtime JS — pure static HTML output
Email-safe HTMLTable-based layouts, inline styles, Outlook VML, Gmail clip checks
Browser + NodeSame API everywhere — CDN, bundler, or CLI
Component systemmc-section, mc-column, mc-text, mc-button, mc-image, mc-hero, mc-list, …
Two styling modesAttribute-based (color="#0066cc") by default; opt-in Tailwind class mode for utility-first teams
Theming & design tokens12 extendable token scales — colors, spacing, fonts, radii, etc.
Templating{{variable}}, mc-if, mc-each, sandboxed (no eval, no Function)
caniemail integrationPer-property compatibility checks against your declared targetClients
Source mapsEvery output element traces back to its source node — bidirectional
AI-friendlyStructured FixInstruction errors, JSON IR, and an MCP server for AI agents
Plugin APIdefineComponent({ type, metadata, compile }) returns a Plugin value — pass to compile(src, { plugins }) or createCompiler({ plugins }). Stateless, multi-instance, no globals.

Two styling modes

mailc supports two styling paradigms. Pick what fits your team — or mix.

Attribute mode (default)

Familiar HTML-style API. CSS-property attributes go directly on components.

<mc-textcolor="#333" font-size="16px" padding="12px 0">
Hello world
</mc-text>

Class mode (limited support)

Tailwind-style utilities for teams that prefer utility-first. CSS-property attributes are flagged as warnings — you express styling via class="" instead.

<mc-textclass="text-[#333] text-[16px] py-3">
Hello world
</mc-text>
compile(source,{templateStyle: 'class'})

Some attributes (e.g. border shorthands, inner-background-color) have no Tailwind equivalent today and remain attribute-only even in class mode.


Custom components (plugins)

defineComponent() returns a plain Plugin value. Pass it to compile() per call, or bind it once with createCompiler(). There is no global registration — plugins are values.

import{defineComponent,compile,createCompiler}from'mailc';constproductCard=defineComponent({type: 'acme-product-card',// must contain `-`, must not start with `mc-`metadata: {description: 'Product card with image, title, and CTA.',category: 'content',parent: 'mc-column',maxChildren: 0,allowsTextContent: false,compilerOutputElements: ['table','tr','td'],compilerOutputReason: 'Centered, email-safe table.',validClassCategories: [],commonMistakes: [],attributes: {title: {type: 'string',required: true,description: 'Product title.',example: 'Acme Widget',hasEmailCompatibilityNotes: false},price: {type: 'string',required: true,description: 'Display price.',example: '$19.99',hasEmailCompatibilityNotes: false},},},compile: (node)=>{consttitle=node.attributes['title']??'';constprice=node.attributes['price']??'';return`<table><tr><td>${title}</td><td>${price}</td></tr></table>`;},});// Per-call:compile(source,{plugins: [productCard]});// Bind once for many calls:constmailc=createCompiler({plugins: [productCard]});mailc.compile(welcome);mailc.compile(receipt);// Multi-instance (different tenants, isolated plugins, same process):consttenantA=createCompiler({plugins: [productCard,tenantAExtras]});consttenantB=createCompiler({plugins: [tenantBExtras]});

The compile function emits HTML as-is — plugins are responsible for escaping user-controlled strings (escapeHtml is exported as a helper) and for any class-mode enforcement (assertClassModeAttributes is exported too). See examples/plugin-product-card for a worked example.


MCP server

mailc ships an MCP server so AI agents (Claude Desktop, Cursor, etc.) can author and validate emails with structured tools instead of guessing at email markup.

// ~/.cursor/mcp.json or ~/.claude/claude_desktop_config.json
{
"mcpServers": {
"mailc": {
"command": "npx",
"args": ["-y", "mailc-mcp"]
}
}
}

Exposes 7 tools: compile_email, validate_email_node, list_components, get_component_spec, can_nest, extract_data_contract, check_email_client_support.

Once configured, ask your agent "build me a welcome email using mailc" and it'll use the tools to compile, validate, and self-correct using structured FixInstruction errors.


Built for tools, not just for templates

mailc was designed so other tools can build on top of it. If you're building an email builder, design system, or AI integration:

  • JSON IRcompileFromJSON() skips the markup parser entirely. Builder state is the input.
  • Source maps — every output HTML element maps back to its source node with byte-precise ranges.
  • Introspection APIintrospect.canNest(), introspect.componentSpec(), introspect.dataContract(), and 8 more functions. Query the compiler instead of reading the docs.
  • Structured errorsFixInstruction objects with action codes (add-attribute, wrap-in, replace-with-class, …) so a UI can render auto-fix buttons.
  • Native dynamic templating{{variable}}, mc-if, mc-each are part of the compiler, not a separate layer. Pass data to compile() and the contract is statically derivable via extract_data_contract.
  • Tailwind-style class mode — opt in with templateStyle: 'class' and write class="text-[#333] py-3" instead of attributes. Same compiler, same output guarantees.
  • caniemail validation — declare targetClients and the compiler flags properties that won't render in your audience's clients, with per-property compatibility data sourced from caniemail.

Try it: the playground's introspection demo shows all of these live.


Playground

A full interactive playground with builder, theme studio, dynamic-email previews, marketplace plugins, and an introspection sandbox.

git clone https://github.com/amareshsm/mailc
cd mailc/playground
pnpm install && pnpm dev

Documentation

Full docs live at the mailc site — getting started, components reference, CLI/API, theming, templating, accessibility, JSON IR, and plugin authoring.


Contributing

Contributions, issues, and feedback are very welcome. Particularly useful at this stage:

  • Try it on a real email and tell me what broke. That's the highest-signal feedback.
  • Tell me what's confusing in the docs. I'm too close to it.
  • Open an issue if a component doesn't compile the way you expect. The CSS-stripping decisions are deliberate but sometimes wrong.
git clone https://github.com/amareshsm/mailc
cd mailc
pnpm install
pnpm test

Credits

mailc is heavily inspired by MJML. Several proven layout decisions were borrowed directly. The snapshot test suite also uses MJML as the reference implementation that mailc's output is checked against to catch regressions.


License

MIT


mailc is early-stage. The compiler works, but the ecosystem around it is still forming. If you'd like to help shape where it goes, the most useful thing you can do is try it on something real and share what you learn.

About

mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - amareshsm/mailc: mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating. · GitHub
Skip to content

Repository files navigation

mailc

A modern email markup compiler — attribute or Tailwind styling, email-safe, developer-first with native dynamic templating.

Write emails as components. Compile to HTML that works in Gmail, Outlook, Apple Mail, and the rest of the long tail. Use Tailwind classes if you want; use plain CSS attributes if you don't.

<mc><mc-body><mc-sectionpadding="32px 0"><mc-column><mc-textfont-size="24px" font-weight="bold" align="center">
Welcome aboard!
</mc-text><mc-buttonhref="https://example.com" background-color="#0066cc" color="#ffffff">
Get Started
</mc-button></mc-column></mc-section></mc-body></mc>
import{compile}from'mailc'const{ html, errors, warnings }=compile(source)// html is ready to send. errors/warnings tell you what's broken.

That's the whole API for the common case.

Status: early. The compiler is solid (3,700+ tests, all passing) but the user base is small. If you try it and hit something rough, please open an issue — feedback at this stage is the most useful contribution you can make.


Why mailc

Email HTML is awful. You can't use flexbox, grid, or modern CSS. You need tables nested inside tables. Outlook needs separate VML markup for backgrounds. Gmail strips <style> blocks under certain conditions. mailc handles all of that and gives you a clean component model on top.

You'd reach for mailc when:

  • You want a TypeScript-native compiler with structured errors, not a black-box CLI
  • You want to use Tailwind utilities, attribute-based styling, or both
  • You want to build a tool on top of an email compiler — JSON IR, source maps, plugin API, AI-agent integration are first-class

Install

npm install mailc
# or
pnpm add mailc
# or
yarn add mailc

Requires Node 20+ for the CLI / Node API. The browser bundle works in any modern browser.


Quick start

1. Compile from a string

import{compile}from'mailc'constresult=compile(` <mc> <mc-body> <mc-section> <mc-column> <mc-text>Hello {{user.name}}</mc-text> </mc-column> </mc-section> </mc-body> </mc>`,{data: {user: {name: 'Ada'}},})console.log(result.html)// production-ready HTMLconsole.log(result.errors)// [] when cleanconsole.log(result.warnings)// non-blocking issues

2. Compile from JSON (for builder UIs)

import{compileFromJSON}from'mailc'constresult=compileFromJSON({type: 'mc',children: [{type: 'mc-body',children: [/* ... */],},],})

3. CLI

mailc build welcome.mc -o welcome.html
mailc validate welcome.mc
mailc watch welcome.mc -o welcome.html --serve # live-reload preview server
mailc init # scaffold a new project
mailc contract welcome.mc # show the data contract

4. Browser

<scriptsrc="https://cdn.jsdelivr.net/npm/mailc/dist/browser.global.js"></script><script>const{ html }=mailc.compile(source)</script>

Core capabilities

Compile-time renderingNo runtime JS — pure static HTML output
Email-safe HTMLTable-based layouts, inline styles, Outlook VML, Gmail clip checks
Browser + NodeSame API everywhere — CDN, bundler, or CLI
Component systemmc-section, mc-column, mc-text, mc-button, mc-image, mc-hero, mc-list, …
Two styling modesAttribute-based (color="#0066cc") by default; opt-in Tailwind class mode for utility-first teams
Theming & design tokens12 extendable token scales — colors, spacing, fonts, radii, etc.
Templating{{variable}}, mc-if, mc-each, sandboxed (no eval, no Function)
caniemail integrationPer-property compatibility checks against your declared targetClients
Source mapsEvery output element traces back to its source node — bidirectional
AI-friendlyStructured FixInstruction errors, JSON IR, and an MCP server for AI agents
Plugin APIdefineComponent({ type, metadata, compile }) returns a Plugin value — pass to compile(src, { plugins }) or createCompiler({ plugins }). Stateless, multi-instance, no globals.

Two styling modes

mailc supports two styling paradigms. Pick what fits your team — or mix.

Attribute mode (default)

Familiar HTML-style API. CSS-property attributes go directly on components.

<mc-textcolor="#333" font-size="16px" padding="12px 0">
Hello world
</mc-text>

Class mode (limited support)

Tailwind-style utilities for teams that prefer utility-first. CSS-property attributes are flagged as warnings — you express styling via class="" instead.

<mc-textclass="text-[#333] text-[16px] py-3">
Hello world
</mc-text>
compile(source,{templateStyle: 'class'})

Some attributes (e.g. border shorthands, inner-background-color) have no Tailwind equivalent today and remain attribute-only even in class mode.


Custom components (plugins)

defineComponent() returns a plain Plugin value. Pass it to compile() per call, or bind it once with createCompiler(). There is no global registration — plugins are values.

import{defineComponent,compile,createCompiler}from'mailc';constproductCard=defineComponent({type: 'acme-product-card',// must contain `-`, must not start with `mc-`metadata: {description: 'Product card with image, title, and CTA.',category: 'content',parent: 'mc-column',maxChildren: 0,allowsTextContent: false,compilerOutputElements: ['table','tr','td'],compilerOutputReason: 'Centered, email-safe table.',validClassCategories: [],commonMistakes: [],attributes: {title: {type: 'string',required: true,description: 'Product title.',example: 'Acme Widget',hasEmailCompatibilityNotes: false},price: {type: 'string',required: true,description: 'Display price.',example: '$19.99',hasEmailCompatibilityNotes: false},},},compile: (node)=>{consttitle=node.attributes['title']??'';constprice=node.attributes['price']??'';return`<table><tr><td>${title}</td><td>${price}</td></tr></table>`;},});// Per-call:compile(source,{plugins: [productCard]});// Bind once for many calls:constmailc=createCompiler({plugins: [productCard]});mailc.compile(welcome);mailc.compile(receipt);// Multi-instance (different tenants, isolated plugins, same process):consttenantA=createCompiler({plugins: [productCard,tenantAExtras]});consttenantB=createCompiler({plugins: [tenantBExtras]});

The compile function emits HTML as-is — plugins are responsible for escaping user-controlled strings (escapeHtml is exported as a helper) and for any class-mode enforcement (assertClassModeAttributes is exported too). See examples/plugin-product-card for a worked example.


MCP server

mailc ships an MCP server so AI agents (Claude Desktop, Cursor, etc.) can author and validate emails with structured tools instead of guessing at email markup.

// ~/.cursor/mcp.json or ~/.claude/claude_desktop_config.json
{
"mcpServers": {
"mailc": {
"command": "npx",
"args": ["-y", "mailc-mcp"]
}
}
}

Exposes 7 tools: compile_email, validate_email_node, list_components, get_component_spec, can_nest, extract_data_contract, check_email_client_support.

Once configured, ask your agent "build me a welcome email using mailc" and it'll use the tools to compile, validate, and self-correct using structured FixInstruction errors.


Built for tools, not just for templates

mailc was designed so other tools can build on top of it. If you're building an email builder, design system, or AI integration:

  • JSON IRcompileFromJSON() skips the markup parser entirely. Builder state is the input.
  • Source maps — every output HTML element maps back to its source node with byte-precise ranges.
  • Introspection APIintrospect.canNest(), introspect.componentSpec(), introspect.dataContract(), and 8 more functions. Query the compiler instead of reading the docs.
  • Structured errorsFixInstruction objects with action codes (add-attribute, wrap-in, replace-with-class, …) so a UI can render auto-fix buttons.
  • Native dynamic templating{{variable}}, mc-if, mc-each are part of the compiler, not a separate layer. Pass data to compile() and the contract is statically derivable via extract_data_contract.
  • Tailwind-style class mode — opt in with templateStyle: 'class' and write class="text-[#333] py-3" instead of attributes. Same compiler, same output guarantees.
  • caniemail validation — declare targetClients and the compiler flags properties that won't render in your audience's clients, with per-property compatibility data sourced from caniemail.

Try it: the playground's introspection demo shows all of these live.


Playground

A full interactive playground with builder, theme studio, dynamic-email previews, marketplace plugins, and an introspection sandbox.

git clone https://github.com/amareshsm/mailc
cd mailc/playground
pnpm install && pnpm dev

Documentation

Full docs live at the mailc site — getting started, components reference, CLI/API, theming, templating, accessibility, JSON IR, and plugin authoring.


Contributing

Contributions, issues, and feedback are very welcome. Particularly useful at this stage:

  • Try it on a real email and tell me what broke. That's the highest-signal feedback.
  • Tell me what's confusing in the docs. I'm too close to it.
  • Open an issue if a component doesn't compile the way you expect. The CSS-stripping decisions are deliberate but sometimes wrong.
git clone https://github.com/amareshsm/mailc
cd mailc
pnpm install
pnpm test

Credits

mailc is heavily inspired by MJML. Several proven layout decisions were borrowed directly. The snapshot test suite also uses MJML as the reference implementation that mailc's output is checked against to catch regressions.


License

MIT


mailc is early-stage. The compiler works, but the ecosystem around it is still forming. If you'd like to help shape where it goes, the most useful thing you can do is try it on something real and share what you learn.

About

mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - amareshsm/mailc: mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating. · GitHub
Skip to content

Repository files navigation

mailc

A modern email markup compiler — attribute or Tailwind styling, email-safe, developer-first with native dynamic templating.

Write emails as components. Compile to HTML that works in Gmail, Outlook, Apple Mail, and the rest of the long tail. Use Tailwind classes if you want; use plain CSS attributes if you don't.

<mc><mc-body><mc-sectionpadding="32px 0"><mc-column><mc-textfont-size="24px" font-weight="bold" align="center">
Welcome aboard!
</mc-text><mc-buttonhref="https://example.com" background-color="#0066cc" color="#ffffff">
Get Started
</mc-button></mc-column></mc-section></mc-body></mc>
import{compile}from'mailc'const{ html, errors, warnings }=compile(source)// html is ready to send. errors/warnings tell you what's broken.

That's the whole API for the common case.

Status: early. The compiler is solid (3,700+ tests, all passing) but the user base is small. If you try it and hit something rough, please open an issue — feedback at this stage is the most useful contribution you can make.


Why mailc

Email HTML is awful. You can't use flexbox, grid, or modern CSS. You need tables nested inside tables. Outlook needs separate VML markup for backgrounds. Gmail strips <style> blocks under certain conditions. mailc handles all of that and gives you a clean component model on top.

You'd reach for mailc when:

  • You want a TypeScript-native compiler with structured errors, not a black-box CLI
  • You want to use Tailwind utilities, attribute-based styling, or both
  • You want to build a tool on top of an email compiler — JSON IR, source maps, plugin API, AI-agent integration are first-class

Install

npm install mailc
# or
pnpm add mailc
# or
yarn add mailc

Requires Node 20+ for the CLI / Node API. The browser bundle works in any modern browser.


Quick start

1. Compile from a string

import{compile}from'mailc'constresult=compile(` <mc> <mc-body> <mc-section> <mc-column> <mc-text>Hello {{user.name}}</mc-text> </mc-column> </mc-section> </mc-body> </mc>`,{data: {user: {name: 'Ada'}},})console.log(result.html)// production-ready HTMLconsole.log(result.errors)// [] when cleanconsole.log(result.warnings)// non-blocking issues

2. Compile from JSON (for builder UIs)

import{compileFromJSON}from'mailc'constresult=compileFromJSON({type: 'mc',children: [{type: 'mc-body',children: [/* ... */],},],})

3. CLI

mailc build welcome.mc -o welcome.html
mailc validate welcome.mc
mailc watch welcome.mc -o welcome.html --serve # live-reload preview server
mailc init # scaffold a new project
mailc contract welcome.mc # show the data contract

4. Browser

<scriptsrc="https://cdn.jsdelivr.net/npm/mailc/dist/browser.global.js"></script><script>const{ html }=mailc.compile(source)</script>

Core capabilities

Compile-time renderingNo runtime JS — pure static HTML output
Email-safe HTMLTable-based layouts, inline styles, Outlook VML, Gmail clip checks
Browser + NodeSame API everywhere — CDN, bundler, or CLI
Component systemmc-section, mc-column, mc-text, mc-button, mc-image, mc-hero, mc-list, …
Two styling modesAttribute-based (color="#0066cc") by default; opt-in Tailwind class mode for utility-first teams
Theming & design tokens12 extendable token scales — colors, spacing, fonts, radii, etc.
Templating{{variable}}, mc-if, mc-each, sandboxed (no eval, no Function)
caniemail integrationPer-property compatibility checks against your declared targetClients
Source mapsEvery output element traces back to its source node — bidirectional
AI-friendlyStructured FixInstruction errors, JSON IR, and an MCP server for AI agents
Plugin APIdefineComponent({ type, metadata, compile }) returns a Plugin value — pass to compile(src, { plugins }) or createCompiler({ plugins }). Stateless, multi-instance, no globals.

Two styling modes

mailc supports two styling paradigms. Pick what fits your team — or mix.

Attribute mode (default)

Familiar HTML-style API. CSS-property attributes go directly on components.

<mc-textcolor="#333" font-size="16px" padding="12px 0">
Hello world
</mc-text>

Class mode (limited support)

Tailwind-style utilities for teams that prefer utility-first. CSS-property attributes are flagged as warnings — you express styling via class="" instead.

<mc-textclass="text-[#333] text-[16px] py-3">
Hello world
</mc-text>
compile(source,{templateStyle: 'class'})

Some attributes (e.g. border shorthands, inner-background-color) have no Tailwind equivalent today and remain attribute-only even in class mode.


Custom components (plugins)

defineComponent() returns a plain Plugin value. Pass it to compile() per call, or bind it once with createCompiler(). There is no global registration — plugins are values.

import{defineComponent,compile,createCompiler}from'mailc';constproductCard=defineComponent({type: 'acme-product-card',// must contain `-`, must not start with `mc-`metadata: {description: 'Product card with image, title, and CTA.',category: 'content',parent: 'mc-column',maxChildren: 0,allowsTextContent: false,compilerOutputElements: ['table','tr','td'],compilerOutputReason: 'Centered, email-safe table.',validClassCategories: [],commonMistakes: [],attributes: {title: {type: 'string',required: true,description: 'Product title.',example: 'Acme Widget',hasEmailCompatibilityNotes: false},price: {type: 'string',required: true,description: 'Display price.',example: '$19.99',hasEmailCompatibilityNotes: false},},},compile: (node)=>{consttitle=node.attributes['title']??'';constprice=node.attributes['price']??'';return`<table><tr><td>${title}</td><td>${price}</td></tr></table>`;},});// Per-call:compile(source,{plugins: [productCard]});// Bind once for many calls:constmailc=createCompiler({plugins: [productCard]});mailc.compile(welcome);mailc.compile(receipt);// Multi-instance (different tenants, isolated plugins, same process):consttenantA=createCompiler({plugins: [productCard,tenantAExtras]});consttenantB=createCompiler({plugins: [tenantBExtras]});

The compile function emits HTML as-is — plugins are responsible for escaping user-controlled strings (escapeHtml is exported as a helper) and for any class-mode enforcement (assertClassModeAttributes is exported too). See examples/plugin-product-card for a worked example.


MCP server

mailc ships an MCP server so AI agents (Claude Desktop, Cursor, etc.) can author and validate emails with structured tools instead of guessing at email markup.

// ~/.cursor/mcp.json or ~/.claude/claude_desktop_config.json
{
"mcpServers": {
"mailc": {
"command": "npx",
"args": ["-y", "mailc-mcp"]
}
}
}

Exposes 7 tools: compile_email, validate_email_node, list_components, get_component_spec, can_nest, extract_data_contract, check_email_client_support.

Once configured, ask your agent "build me a welcome email using mailc" and it'll use the tools to compile, validate, and self-correct using structured FixInstruction errors.


Built for tools, not just for templates

mailc was designed so other tools can build on top of it. If you're building an email builder, design system, or AI integration:

  • JSON IRcompileFromJSON() skips the markup parser entirely. Builder state is the input.
  • Source maps — every output HTML element maps back to its source node with byte-precise ranges.
  • Introspection APIintrospect.canNest(), introspect.componentSpec(), introspect.dataContract(), and 8 more functions. Query the compiler instead of reading the docs.
  • Structured errorsFixInstruction objects with action codes (add-attribute, wrap-in, replace-with-class, …) so a UI can render auto-fix buttons.
  • Native dynamic templating{{variable}}, mc-if, mc-each are part of the compiler, not a separate layer. Pass data to compile() and the contract is statically derivable via extract_data_contract.
  • Tailwind-style class mode — opt in with templateStyle: 'class' and write class="text-[#333] py-3" instead of attributes. Same compiler, same output guarantees.
  • caniemail validation — declare targetClients and the compiler flags properties that won't render in your audience's clients, with per-property compatibility data sourced from caniemail.

Try it: the playground's introspection demo shows all of these live.


Playground

A full interactive playground with builder, theme studio, dynamic-email previews, marketplace plugins, and an introspection sandbox.

git clone https://github.com/amareshsm/mailc
cd mailc/playground
pnpm install && pnpm dev

Documentation

Full docs live at the mailc site — getting started, components reference, CLI/API, theming, templating, accessibility, JSON IR, and plugin authoring.


Contributing

Contributions, issues, and feedback are very welcome. Particularly useful at this stage:

  • Try it on a real email and tell me what broke. That's the highest-signal feedback.
  • Tell me what's confusing in the docs. I'm too close to it.
  • Open an issue if a component doesn't compile the way you expect. The CSS-stripping decisions are deliberate but sometimes wrong.
git clone https://github.com/amareshsm/mailc
cd mailc
pnpm install
pnpm test

Credits

mailc is heavily inspired by MJML. Several proven layout decisions were borrowed directly. The snapshot test suite also uses MJML as the reference implementation that mailc's output is checked against to catch regressions.


License

MIT


mailc is early-stage. The compiler works, but the ecosystem around it is still forming. If you'd like to help shape where it goes, the most useful thing you can do is try it on something real and share what you learn.

About

mailc [WIP]- A modern email markup compiler — Tailwind-powered, email-safe, with native dynamic templating.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages