Skip to content

Repository files navigation

html-email logohtml-email logo

html-email

HTML email that renders everywhere — classic Outlook to dark mode — with no compiler in the way.

CIOpenSSF ScorecardSLSA Build L3DependenciesNodeLicense: MIT

QuickstartWhyHow it buildsLint any emailTemplatesDocs

Live demo →


Quickstart

Node ≥ 16. No install needed to build or lint.

git clone https://github.com/dcondrey/html-email.git &&cd html-email
node framework/build/build.mjs # partials + content.json -> framework/dist/email.html
node framework/build/lint.mjs --profile house framework/dist/email.html

That is the whole loop. npm ci is only needed for screenshots (npm run previews) and the render smoke-test (npm run smoke).

Tip

Three ways in, all producing identical markup: copy framework/template.html (one commented file with {{placeholders}}), paste the components you need from framework/partials/, or run the build above.

Why html-email

Most frameworks quietly drop the awkward clients because supporting them is tedious. This one does not. It began in 2014 as "the most cross-compatible template you'll find anywhere" and now covers both that client landscape and everything since.

What it means
Never drop supportNo workaround breaks an older client to help a newer one. 28 documented quirks, defended in the markup and enforced by 28 lint rules.
Zero dependenciesA ~120-line build.mjs concatenates partials and injects content.json. No MJML, no compiler, no fidelity loss. Puppeteer and fast-check are dev-only.
Verified, not just authoredThe linter gates the HTML, a self-test gates the linter, a fuzzer gates its edge cases, and smoke.mjs gates the actual render in headless Chrome.
Works on anyone's emailThe default rule set is client behaviour every email is subject to, not this project's conventions. Point it at MJML, React Email, or Maizzle output.
Dark mode, three waysprefers-color-scheme, Outlook.com [data-ogsc]/[data-ogsb], and an explicit background-color on every container the dark CSS repaints.
Desktop — lightDesktop — darkMobile
LightDarkMobile

Against the compiler frameworks

Checked against each project's own documentation, 2026-08-22. These are all good tools; the axis that separates them is whether a compiler stands between you and the markup that ships.

html-emailMJMLMaizzleReact Email
You authorHTML<mj-*> markup languageVue SFCs + TailwindReact/JSX components
Build step to get HTMLoptional (concatenation only)required (compile)required (Vite pipeline)required (render)
Runtime dependencies0npm packageNode + Vite + TailwindNode + React
Ships markup you wroteyesno — compiler outputno — pipeline outputno — render output
Hand-editing the shipped HTMLexpectedround-trip is lostround-trip is lostround-trip is lost
CSS inliningnot needed (styles authored in place)automaticconfigurable transformersvia Tailwind component
Lints other tools' outputyes — point it at any HTML emailnonono

The trade is real and runs both ways. A compiler gives you components, scoping, and a smaller surface to get wrong, and for a large template library that is worth a lot. This project takes the other side: the HTML in the repository is the HTML that arrives, so a client-specific quirk is fixed where you can see it, and nothing has to survive a regeneration. The 28 documented quirks are the reason that trade is worth making here.

How it builds

flowchart LR
P["partials/*.html"] --> B["build.mjs"]
C["content.json"] --> B
B --> D["dist/email.html"]
B -. "--production" .-> M["dist/email.min.html"]
D --> L{"lint.mjs"}
L -->|"--profile universal"| U["client rules<br/>any email"]
L -->|"--profile house"| H["+ framework invariants"]
D --> S["smoke.mjs<br/>headless Chrome, light + dark"]
Loading

The output is the hand-tuned partials verbatim. Nothing is translated, so nothing is lost on the oldest clients.

Lint any email

The linter has no runtime dependencies and works on any HTML email, whatever produced it.

node framework/build/lint.mjs campaign.html # any HTML email
node framework/build/lint.mjs --json dist/*.html # machine-readable
node framework/build/lint.mjs --sarif campaign.html # upload to code scanning
ProfileRulesUse for
universal(default)14 rules — Gmail's 102 KB clip and 8 KB <style> cap, unbalanced CSS, missing alt/width, MSO conditional balance, unsubscribe link, role="presentation", viewport/charset, inline images, CSS background images, dark-mode inversion riskAny HTML email, whoever built it
houseThe above plus 14 framework invariants — both dark-mode metas, <o:PixelsPerInch>96, a prefers-color-scheme block, .ExternalClass, mso-table-lspace, text-size-adjust, iOS auto-linking, Apple reformatting, Outlook.com dark mode, MSO font fallback, explicit dark backgrounds, a hidden preheader, the attribution noticeThis repo's templates, or a fork of its conventions

Warning

--profile house is what npm run lint and CI use. Leave it off a file in this repo and fourteen real invariants pass silently.

Two layers.lint.mjs validates the HTML; npm run smoke validates the render — it loads every build in headless Chrome, light and dark, and fails on a broken image, horizontal overflow, or page error. That is the class of bug a linter cannot see.

Templates

Five distinct designs plus the canonical framework, each in the house conventions, each passing both gates clean.

TemplateDesign
framework/The canonical modern template (header · hero · columns · button · footer)
templates/cairn-wellness/Wellness shop — green/cream/amber, Poppins
templates/ferry-street-brewing/Brewery — amber/brown on dark roast, Oswald · 3-col tasting + gallery
templates/lantern-and-quill/Bookshop — coral+yellow on plum, Playfair · team + product cards
templates/voltline/Electronics, dark-first — dark+pink, Space Grotesk · pricing table
templates/meridian-advisory/Consulting — charcoal+coral on grey, IBM Plex Sans · quote block
examples/northwind-botanicals/The 2014 production reference, scrubbed to a fictitious brand
See the five branded templates
Cairn WellnessFerry Street BrewingLantern & QuillVoltlineMeridian Advisory
Cairn WellnessFerry Street BrewingLantern & QuillVoltlineMeridian Advisory

Brand names and imagery are fictitious mock content; the art is self-authored, generated by scripts/.

Start a new one:

node scripts/new-template.mjs acme-widgets "Acme Widgets"

Scaffolds partials, manifest, content.json, and an asset-generator stub that builds and lints 0/0 out of the box. Then retheme 00-document-open.html, edit the copy, fill in the generator, and wire it into package.json and the CI/release workflows.

Repository layout
html-email/
├── framework/ the maintained, modern template (start here)
│ ├── template.html single-file master, fully commented
│ ├── partials/ the same template as documented components
│ ├── build/ build.mjs · lint.mjs/rules.mjs · tests · content.json
│ ├── dist/ built output (email.html + email.min.html)
│ └── assets/ sample images
│
├── templates/ five distinct branded designs, house conventions
├── examples/ northwind-botanicals + the original 2014 campaigns
├── scripts/ build-template.mjs, preview + asset generators
├── tools/ email-checklist/ — self-contained pre-send checklist
├── docs/ quirks reference, client matrix, testing, previews
└── legacy/ the preserved, unmodified 2014 artifact
Supply chain & provenance

Actions are pinned to full commit SHAs. CodeQL, Dependency Review, OpenSSF Scorecard, and Dependabot run in CI. Tagged releases publish a signed SLSA build-provenance attestation over the release archive (see release.yml):

gh attestation verify html-email-<tag>.zip --repo dcondrey/html-email

Security policy and private reporting: SECURITY.md.

Documentation

GuideWhat's in it
Quirks referenceThe 28 cross-client behaviours the framework defends against, each explained
Platform supportThe client matrix and market data
Testing & ESP integrationLinting, real-client verification, merge tags, one-click unsubscribe
Pre-send checklistA zero-dependency checklist covering setup → design → build → test → send

Contributing · Security · Changelog

License

MIT — use it, adapt it, ship it commercially, no permission needed. © 2014-2026 David Condrey.

MIT asks one thing in return: keep the copyright notice. Every built email carries it for you, as a 178-byte comment after the doctype that survives the production minifier and is invisible to recipients. Complying is automatic — just don't strip it.

Shipping something built on this? A credit line is not required, but it is what keeps the oldest clients supported:

Email templates by html-email, MIT Licensed.

Details, and what you explicitly do not need permission for, in NOTICE.

About

Zero-dependency, hand-authored HTML email framework that never drops support for the oldest clients — MJML alternative with 28 documented quirks.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages