Modern email template compilation and rendering for safe HTML email.
inline-email turns HTML/CSS into an email-safe compiled template. Dynamic values are rendered later without running full CSS inlining for every send.
Build responsive transactional and product emails from small HTML building blocks, plain CSS, and simple runtime data.
Migrating from the old package? Start with the v3 migration guide.
<email>
<preview>{{featureCount}} ACME features your team can start using this week.</preview>
<container>
<section padding="36">
<brand mark="A">ACME</brand>
{{#features}}
<panel tone="{{tone}}">
<p>{{title}}</p>
<p>{{description}}</p>
</panel>
<spacer size="14" />
{{/features}}
<button href="{{featuresUrl}}">See all features</button>
</section>
<footer>Sent by Freethinking IT</footer>
</container>
</email>| Welcome + verify | Product launch |
|---|---|
![]() |
![]() |
| Usage digest | Upgrade offer |
|---|---|
![]() |
![]() |
The repo includes rendered examples for auth, onboarding, product marketing, usage digests, and upgrade offers. Run npm run examples to regenerate them into examples/out.
To open the full example gallery locally:
npm run examples:serveThen visit http://localhost:4173.
For app-shaped examples, see:
examples/integrations/node-send.js: compile once, render with user data, and pass the result to a sender.examples/integrations/preview-server.js: preview a rendered email through a small local HTTP route.
The package targets Node.js 20, 22, and 24.
- CI workflow runs install, high-severity audit, build, tests, and package dry-run on Node.js 20, 22, and 24.
- Release workflow publishes version tags to npm with provenance and attaches the package tarball to a GitHub Release.
- Test coverage is focused in
test/across the template renderer, responsive layout compiler, compile/render API, example templates, and CLI argument handling. - Migration guide covers the v2 to v3 API and CLI changes.
- Local checks:
npm audit --audit-level=high
npm run build
npm test
npm run test:visual
npm pack --dry-runThe example integration test compiles real auth and marketing templates with production-shaped data. Visual regression tests render the baseline emails in headless Chromium and compare screenshots against approved snapshots.
When an intentional layout change updates the screenshots:
npm run test:visual -- --update-snapshotsInstall from npm:
npm install inline-emailCompile the template once at build/deploy time or cold start:
import { compileEmailTemplate } from 'inline-email';
const compiled = await compileEmailTemplate({
subject: 'Welcome to ACME, {{firstName}}',
html,
css,
text: 'Welcome to ACME, {{firstName}}'
});Render dynamic data at email-send time:
const email = compiled.render({
data: {
firstName: 'Dan',
appUrl: 'https://app.acme.example',
slots: {
summary: '<p>Your account was approved.</p>'
}
}
});
// { subject, html, text }One-shot rendering is also available:
import { renderEmail } from 'inline-email';
const email = await renderEmail({
template: {
subject: 'Welcome, {{firstName}}',
html: '<p>Hello {{firstName}}</p>'
},
data: {
firstName: 'Dan'
}
});Normal variables are escaped by default:
<p>Hello {{firstName}}</p>Repeated blocks render arrays:
<ul>
{{#items}}<li>{{label}}</li>{{/items}}
</ul>Trusted HTML slots must be explicit:
<section>{{slot:summary}}</section>Slots are supplied separately via data.slots, which keeps trusted HTML injection intentional instead of relying on accidental unescaped variables.
The compiler includes a small Inky-like responsive layout layer. These reusable tags compile to conservative table markup before CSS is inlined:
<email>
<preview>Welcome to ACME</preview>
<container>
<section padding="32">
<brand href="{{brandUrl}}" mark="A">ACME</brand>
<hero-image src="{{heroImageUrl}}" alt="ACME preview" width="528" />
<spacer size="24" />
<columns>
<column width="50%">
<p>Hello {{firstName}}</p>
</column>
<column width="50%">
<button href="{{appUrl}}">Open ACME</button>
</column>
</columns>
<spacer size="24" />
<alert tone="warning">This link expires in {{expiresIn}}.</alert>
</section>
<footer>Sent by Freethinking IT</footer>
</container>
</email>Supported tags:
<email> Full document wrapper with mobile media rules.
<preview> Hidden inbox preview text.
<container> Centered 600px email body.
<section> Padded content block.
<columns> Table row for columns.
<column> Responsive column that stacks on small screens.
<button> Table-based CTA button.
<brand> Small wordmark/header lockup.
<image> Responsive email-safe image.
<hero-image> Wide responsive image with rounded corners.
<spacer> Email-safe vertical spacing.
<divider> Horizontal rule using table markup.
<otp> One-time code display block.
<alert> Inline warning/info/success/danger notice.
<panel> Bordered content panel.
<footer> Muted footer section.
Responsive layout tags are enabled by default. Disable them with responsive: false if you want to pass through raw HTML unchanged.
<alert> and <panel> include a small vanilla tone set:
<alert tone="warning">This link expires soon.</alert>
<panel tone="success">Your workspace is ready.</panel>Supported tones are neutral, info, success, warning, and danger.
You can render with only data:
compiled.render({
data: {
tone: 'info'
}
});Or override tone colors for a brand:
compiled.render({
data,
style: {
tones: {
info: {
background: '#eff8ff',
border: '#b2ddff',
text: '#1849a9',
accent: '#1570ef'
}
}
}
});inline-email does not create a standalone hidden tracker. For open/load analytics, use normal images with tracking parameters or route useful image URLs through your own image proxy:
<hero-image
src="https://images.example.com/open/welcome.png?messageId={{messageId}}&recipient={{recipientId}}"
alt="Welcome preview"
width="528"
/>That keeps analytics tied to useful email assets such as logos, hero images, and product screenshots instead of relying on a hidden request that many clients block. Your sending app owns consent, disclosure, retention, and analytics handling.
For layout syntax or local experiments, pass a compile-stage transform. It runs before CSS inlining and before runtime values are rendered:
const compiled = await compileEmailTemplate({
html,
css,
transform: (source) => renderResponsiveLayout(source)
});This is where product-specific helpers can grow without coupling the package to Inky, MJML, React Email, or any other layout framework.
The CLI remains available as inline-email:
inline-email input.html
inline-email input.html --out output.html
inline-email --css style.css input.htmlSupported options:
--html <file> Input HTML file. A positional input file is also supported.
--css <files...> CSS files to inline.
--out, -o <file> Write output to a file instead of stdout.
--noInlineImages Disable image web resource inlining.
--force, -f Overwrite the output file.
--help, -h Show help text.
npm install
npm run build
npm testReleases publish to npm and create a GitHub Release. Push a version tag that matches package.json:
git tag v3.0.0
git push origin v3.0.0GitHub Actions runs audit, tests, build, package verification, publishes to npm with provenance, and creates a GitHub Release with the package tarball attached.
Before the first automated release, configure npm Trusted Publishing for this repository and the Release workflow. For a one-time manual first publish, run:
npm publishinline-email does not validate that your markup uses only email-safe HTML tags, and it does not replace unsafe tags for you.




