Hand-authored, zero-dependency, and built on one rule: never abandon a client
while a single real person still uses it — from classic Outlook to dark mode.
Most email frameworks quietly abandon the awkward clients — classic Outlook, Android 4's stock mail, Windows Phone — because supporting them is tedious. html-email does the opposite. It began in 2014 as "the most cross-compatible template you'll find anywhere," and it now covers both the 2014 client landscape and everything since: dark mode, the new Chromium Outlook, Apple Mail Privacy Protection, one-click unsubscribe, and the accessibility expectations of VoiceOver/TalkBack. There is no MJML and no compiler — the built output is the hand-tuned partials verbatim, so nothing is lost in translation on the oldest clients.
- Never-drop-support. Every fix degrades gracefully; no workaround breaks an older client to help a newer one. 28 documented quirks, each defended in the markup.
- Zero dependencies. A ~120-line
build.mjsconcatenates partials and injectscontent.json. No framework, no MJML, no fidelity loss. (Puppeteer is a dev-only dependency for screenshots.) - Verified two ways, not just authored.
lint.mjsgates the HTML — Gmail's 102 KB clip and 8 KB<style>limit, bulletproof buttons, balanced MSO comments,alttext and image widths, dark-mode metas + Outlook DPI pinning, a real unsubscribe link,role="presentation"— and ships with its own self-test. Thennpm run smokerenders every build in headless Chrome (light + dark) and fails on a broken image, horizontal overflow, or page error — the class of bug a HTML linter can't see. - Three ways to use it. Copy the single-file master, assemble the documented partials, or run the build. All three produce identical markup.
- Dark mode, three ways.
prefers-color-scheme, Outlook.com[data-ogsc]/[data-ogsb], and Gmail-safe explicitbgcoloron every cell. - Five branded templates, and a scaffolder. Distinct designs (see below), each rebuilt in the
house conventions with self-authored generated art and passing both gates clean. Start a new one
with
node scripts/new-template.mjs <name> "Brand"— a complete, conformant template out of the box.
| Framework — light | Framework — dark | Mobile | Cairn Wellness template |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
One source, no per-client forks. Screenshots are the Blink render; see Testing for real-client verification. Live demo: https://dcondrey.github.io/html-email/
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 (assembler) + lint.mjs + content.json
│ ├── dist/ built output (email.html + email.min.html)
│ └── assets/ sample images
│
├── templates/ distinct branded designs, house conventions
│ ├── cairn-wellness/ partials + manifest + content.json + assets + dist
│ ├── ferry-street-brewing/ brewery — 3-col tasting + gallery
│ ├── lantern-and-quill/ bookshop — team + product cards
│ ├── voltline/ electronics, dark-first — date-list + pricing table
│ └── meridian-advisory/ consulting — quote block
│
├── examples/
│ ├── northwind-botanicals/ the framework filled out as a real campaign
│ └── campaigns-2014/ original 2014 campaigns, kept as references
│
├── 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
Zero dependencies, Node ≥ 16.
1. Copy the single file. Open framework/template.html — the whole
email in one commented file with {{placeholders}}. Replace them and ship.
2. Assemble the partials. framework/partials/ breaks it into components
(header, hero, columns, button, footer, …). Paste the ones you need in order.
3. Build it.
cd framework/build
node build.mjs # inject content.json → ../dist/email.html
node build.mjs --production # + minify (strips docs, keeps MSO comments)
node lint.mjs ../dist/email.htmlBuild a branded template the same way:
node scripts/build-template.mjs templates/cairn-wellness
node framework/build/lint.mjs templates/cairn-wellness/dist/email.htmlTwo layers of checks. lint.mjs validates the HTML (Gmail size, dark-mode metas,
bulletproof buttons, image widths, …). npm run smoke validates the render: it loads every
built email in headless Chrome (light and dark) and fails on a broken image, horizontal overflow,
or a page error — the class of bug lint cannot see. npm test runs the build + lint + the linter's
own self-test; CI additionally runs the smoke-test.
| Template | Design | Status |
|---|---|---|
framework/ |
The canonical modern template (header · hero · columns · button · footer) | ✅ Maintained |
examples/northwind-botanicals/ |
The 2014 production reference, scrubbed to a fictitious brand | ✅ |
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 / education (coral+yellow on plum, Playfair · team + product cards) | ✅ |
templates/voltline/ |
Electronics sale, dark-first (dark+pink, Space Grotesk · date-list + pricing table) | ✅ |
templates/meridian-advisory/ |
Consulting / corporate (charcoal+coral on grey, IBM Plex Sans · quote block) | ✅ |
| Cairn Wellness | Ferry Street Brewing | Lantern & Quill | Voltline | Meridian Advisory |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
Brand names and imagery are fictitious mock content for demonstration; imagery is self-authored,
generated by scripts/.
Start a new one: node scripts/new-template.mjs <kebab-name> "Brand Name" scaffolds a complete
template (partials, manifest, content.json, asset-generator stub) that builds and lints 0/0 out
of the box. Retheme the palette in 00-document-open.html, edit the copy, fill in the asset
generator, then wire it into package.json and the CI/release workflows.
Actions are pinned to full commit SHAs, and CodeQL, Dependency Review, OpenSSF Scorecard,
and Dependabot run in CI. Tagged releases publish a signed SLSA
build-provenance attestation over the release archive (see .github/workflows/release.yml); verify
it with:
gh attestation verify html-email-<tag>.zip --repo dcondrey/html-emailSecurity policy and private reporting: SECURITY.md.
- Quirks reference — the 28 cross-client behaviours the framework defends against, explained
- Platform support — the client matrix and market data
- Testing & ESP integration — linting, real-client verification, merge tags, one-click unsubscribe
- Pre-send checklist — a self-contained, zero-dependency checklist that walks setup → design → build → test → send (progress saves in your browser)
- Contributing · Security · Changelog
MIT — use it, adapt it, ship it. Attribution appreciated, not required. © 2026 David Condrey.









