How to use, customize, and deploy these email templates in your projects. Covers manual workflows, build tool integration, and automated pipelines.
responsive-email-templates/
├── templates/
│ ├── transactional/ # 10 templates for account & order flows
│ │ ├── welcome.html
│ │ ├── receipt.html
│ │ ├── password-reset.html
│ │ ├── email-verification.html
│ │ ├── account-activated.html
│ │ ├── shipping-notification.html
│ │ ├── invoice.html
│ │ ├── subscription-confirmed.html
│ │ ├── order-cancelled.html
│ │ └── account-deactivated.html
│ ├── marketing/ # 10 templates for campaigns
│ │ ├── promotional-offer.html
│ │ ├── announcement.html
│ │ ├── product-launch.html
│ │ ├── flash-sale.html
│ │ ├── seasonal-campaign.html
│ │ ├── referral-invite.html
│ │ ├── survey-request.html
│ │ ├── re-engagement.html
│ │ ├── webinar-invite.html
│ │ └── loyalty-reward.html
│ └── newsletters/ # 10 templates for recurring content
│ ├── weekly-digest.html
│ ├── monthly-roundup.html
│ ├── featured-content.html
│ ├── industry-news.html
│ ├── tips-and-tricks.html
│ ├── community-update.html
│ ├── curator-picks.html
│ ├── event-recap.html
│ ├── product-changelog.html
│ └── year-in-review.html
├── partials/
│ ├── head-reset.html # CSS resets and head boilerplate
│ └── footer.html # Reusable footer with legal links
├── guides/
│ ├── merge-tags-guide.md # ESP merge tag reference
│ ├── email-client-compatibility.md # Client support matrix
│ ├── testing-checklist.md # Pre-send QA checklist
│ └── build-workflow.md # This file
├── configs/
│ └── litmus-test-config.json # Litmus test automation config
├── README.md
└── LICENSE
The simplest way to use these templates — no build tools required.
Choose the template that matches your email type. Each template is a complete, standalone HTML file — no assembly required.
Open the template in a text editor and update these values:
Search & Replace:
"Acme Corp" → Your company name
"#4a90d9" → Your primary brand color
"#2c5f8a" → Your dark brand color (buttons hover, header)
"#e8f4e8" → Your light accent color
"https://example.com" → Your website URL
Logo image URL → Your hosted logo URL
Follow the merge-tags-guide.md to swap {{placeholder}} syntax for your ESP's format.
Follow the testing-checklist.md before sending to your list.
For teams with a build pipeline, here are patterns for common tools.
MJML is a markup language that compiles to email-compatible HTML. You can use these templates as-is (they're already compiled), but if you want to author new templates in MJML:
# Install MJML CLI
npm install -g mjml
# Compile a single template
mjml input.mjml -o output.html
# Watch for changes during development
mjml --watch input.mjml -o output.htmlTo integrate MJML into a build pipeline:
// package.json
{
"scripts": {
"build:emails": "mjml src/emails/*.mjml -o dist/emails/",
"watch:emails": "mjml --watch src/emails/*.mjml -o dist/emails/",
"preview:email": "open dist/emails/welcome.html"
}
}If you prefer writing styles in a block and inlining them at build time:
npm install juice// scripts/inline-css.js
// Reads HTML with <style> blocks and inlines all CSS properties
// onto the elements they target. Essential for email client compatibility.
const juice = require('juice');
const fs = require('fs');
const path = require('path');
const templatesDir = path.join(__dirname, '..', 'templates');
const outputDir = path.join(__dirname, '..', 'dist');
// Juice options tuned for email
const juiceOptions = {
preserveMediaQueries: true, // Keep @media queries in <style>
preserveFontFaces: true, // Keep @font-face in <style>
preserveKeyFrames: true, // Keep @keyframes in <style>
preservePseudos: true, // Keep :hover etc. in <style>
applyStyleTags: true, // Inline from <style> blocks
removeStyleTags: false, // Keep <style> for media queries
insertPreservedExtraCss: true, // Re-insert preserved CSS
extraCss: '', // Additional CSS to inline
};
function processTemplate(filePath) {
const html = fs.readFileSync(filePath, 'utf-8');
const inlined = juice(html, juiceOptions);
const relativePath = path.relative(templatesDir, filePath);
const outputPath = path.join(outputDir, relativePath);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, inlined, 'utf-8');
console.log(`Inlined: ${relativePath}`);
}
// Process all HTML files in templates/
function walkDir(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkDir(fullPath);
} else if (entry.name.endsWith('.html')) {
processTemplate(fullPath);
}
}
}
walkDir(templatesDir);
console.log('CSS inlining complete.');Minify templates to stay under Gmail's 102KB clipping threshold:
npm install html-minifier-terser// scripts/minify-emails.js
const { minify } = require('html-minifier-terser');
const fs = require('fs');
const path = require('path');
const inputDir = path.join(__dirname, '..', 'dist');
async function minifyFile(filePath) {
const html = fs.readFileSync(filePath, 'utf-8');
const originalSize = Buffer.byteLength(html, 'utf-8');
const minified = await minify(html, {
collapseWhitespace: true,
conservativeCollapse: true, // Collapse to 1 space, not 0
removeComments: false, // Keep MSO conditionals!
removeRedundantAttributes: false, // Keep all attributes for email
minifyCSS: {
level: { 1: { specialComments: 'all' } } // Keep /*! ... */ comments
},
// CRITICAL: Don't remove conditional comments — they're for Outlook
ignoreCustomComments: [/\[if/, /endif/],
});
const newSize = Buffer.byteLength(minified, 'utf-8');
const savings = ((1 - newSize / originalSize) * 100).toFixed(1);
fs.writeFileSync(filePath, minified, 'utf-8');
console.log(`${path.basename(filePath)}: ${originalSize}B → ${newSize}B (${savings}% smaller)`);
}
// Process all HTML files
async function processAll() {
const files = fs.readdirSync(inputDir, { recursive: true })
.filter(f => f.endsWith('.html'))
.map(f => path.join(inputDir, f));
for (const file of files) {
await minifyFile(file);
}
}
processAll();The partials/ directory contains reusable HTML fragments. Since email HTML doesn't support includes natively, here are three ways to use them:
Open partials/head-reset.html, copy the content, and paste it into the of each template you customize. Simple but tedious.
Use a simple Node.js script to process include directives:
// scripts/process-includes.js
// Replaces <!-- @include partials/head-reset.html --> with file contents
const fs = require('fs');
const path = require('path');
function processIncludes(html, baseDir) {
// Match <!-- @include path/to/file.html --> directives
return html.replace(
/<!--\s*@include\s+(.+?)\s*-->/g,
(match, includePath) => {
const fullPath = path.join(baseDir, includePath.trim());
if (!fs.existsSync(fullPath)) {
console.warn(`Include not found: ${fullPath}`);
return match;
}
return fs.readFileSync(fullPath, 'utf-8');
}
);
}
// Usage: node scripts/process-includes.js templates/transactional/welcome.html
const inputFile = process.argv[2];
if (!inputFile) {
console.error('Usage: node process-includes.js <template.html>');
process.exit(1);
}
const html = fs.readFileSync(inputFile, 'utf-8');
const processed = processIncludes(html, path.dirname(inputFile));
const outputPath = inputFile.replace('templates/', 'dist/');
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, processed, 'utf-8');
console.log(`Processed: ${inputFile} → ${outputPath}`);For larger projects, use a proper templating engine:
npm install nunjucks// scripts/render-nunjucks.js
const nunjucks = require('nunjucks');
const fs = require('fs');
const path = require('path');
// Configure Nunjucks to look for partials in the project root
const env = nunjucks.configure(path.join(__dirname, '..'), {
autoescape: false, // Don't escape HTML — we're generating HTML
});
// Render a template with variables
const html = env.render('templates/transactional/welcome.html', {
company_name: 'Your Company',
first_name: 'Alex',
login_url: 'https://app.example.com/login',
});
fs.writeFileSync('dist/welcome.html', html);Then in your template:
{% include "partials/head-reset.html" %}
<!-- ... template content ... -->
{% include "partials/footer.html" %}Open any template directly in your browser:
# macOS
open templates/transactional/welcome.html
# Linux
xdg-open templates/transactional/welcome.html
# Or use a local server for auto-reload
npx live-server templates/ --port=3000Caveat: Browser rendering ≠ email client rendering. Always test in actual email clients before sending.
Use these methods to send yourself actual test emails:
# Using PutsMail (free, web-based)
# 1. Go to https://putsmail.com/
# 2. Paste your HTML
# 3. Enter your email address
# 4. Hit send
# Using nodemailer (Node.js)
npm install nodemailer// scripts/send-test.js
const nodemailer = require('nodemailer');
const fs = require('fs');
// For testing: use Ethereal (fake SMTP that captures emails)
// In production: replace with your real SMTP credentials
async function sendTest(templatePath, toAddress) {
const testAccount = await nodemailer.createTestAccount();
const transporter = nodemailer.createTransport({
host: 'smtp.ethereal.email',
port: 587,
secure: false,
auth: {
user: testAccount.user,
pass: testAccount.pass,
},
});
const html = fs.readFileSync(templatePath, 'utf-8');
const info = await transporter.sendMail({
from: '"Test Sender" <test@example.com>',
to: toAddress || testAccount.user,
subject: `Test: ${templatePath}`,
html: html,
});
console.log('Message sent:', info.messageId);
console.log('Preview URL:', nodemailer.getTestMessageUrl(info));
}
const template = process.argv[2] || 'templates/transactional/welcome.html';
sendTest(template);# .github/workflows/email-lint.yml
name: Email Template Validation
on:
pull_request:
paths:
- 'templates/**'
- 'partials/**'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check HTML validity
run: |
npm install -g html-validate
html-validate "templates/**/*.html" --config .htmlvalidate.json
- name: Check file sizes (Gmail 102KB limit)
run: |
for file in templates/**/*.html; do
size=$(wc -c < "$file")
if [ "$size" -gt 104857 ]; then
echo "WARNING: $file is ${size} bytes (over 102KB Gmail limit)"
exit 1
fi
done
- name: Check for unfilled placeholders
run: |
# Make sure no raw {{placeholder}} markers were left in dist/
if grep -r '{{.*}}' dist/ 2>/dev/null; then
echo "ERROR: Unfilled placeholders found in dist/"
exit 1
fiDeploy your email templates to a static site for team review:
# .github/workflows/deploy-previews.yml
name: Deploy Email Previews
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate index page
run: |
echo '<!DOCTYPE html><html><body><h1>Email Templates</h1><ul>' > dist/index.html
for file in templates/**/*.html; do
name=$(basename "$file" .html)
category=$(basename $(dirname "$file"))
echo "<li><a href=\"$file\">$category / $name</a></li>" >> dist/index.html
done
echo '</ul></body></html>' >> dist/index.html
# Deploy to your preferred static host
- name: Deploy to Netlify / Vercel / GitHub Pages
run: echo "Add your deployment step here"When you're ready to use a template in production:
1. Brand customization — Colors, logo, company name, links
2. Merge tag conversion — Swap {{placeholders}} for your ESP's syntax (see merge-tags-guide.md)
3. CSS inlining — If you modified blocks, re-inline with Juice
4. Minification — Optional but recommended for large templates
5. Testing — Full checklist in testing-checklist.md
6. Spam score — Check with Mail-Tester (aim for 8+/10)
7. Send test — Real email to yourself with live merge data
8. Peer review — Fresh eyes catch rendering issues you've gone blind to
9. Segment test — Send to a small segment first before full list
10. Monitor — Watch open rates, click rates, bounce rates, and spam complaints
Part of Frontend Developer Pro
A comprehensive reference for which CSS properties, HTML elements, and rendering behaviors work across email clients. Use this to understand why these templates are built the way they are, and what to watch for when customizing.
| Feature | Apple Mail | Outlook 2019+ | Outlook 2016 | Thunderbird | Windows Mail | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ✅ | ⚠️ | ❌ | ✅ | ✅ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|