Comprehensive guide for theming and styling Magento 2 transactional emails via theme files, including Hyvรค-specific considerations, CSS inlining architecture, and template override patterns.
This skill provides comprehensive guidance on theming and styling Magento 2 transactional emails via theme files. It covers the full email rendering pipeline, CSS inlining architecture, template override patterns, Hyvรค Email module integration, and the Tailwind-to-LESS compilation approach.
1. Module defines template in email_templates.xml (e.g. sales_email_order_template)
โ
2. TransportBuilder triggers template rendering
โ
3. AbstractTemplate::getProcessedTemplate()
- applyDesignConfig() โ emulates store/theme context
- addEmailVariables() โ populates store data, template_styles
โ
4. Filter->filter(templateText) processes directives:
- {{template config_path="design/email/header_template"}} โ includes header
- {{template config_path="design/email/footer_template"}} โ includes footer
- {{css file="css/email.css"}} โ outputs CSS in <style> tag
- {{inlinecss file="css/email-inline.css"}} โ queues CSS for inlining
- {{var variable}} โ outputs variable values
- {{trans "text"}} โ translatable strings
- {{layout handle="..."}} โ renders layout blocks
โ
5. applyInlineCss() callback:
- Loads compiled CSS via asset repository
- Processes CSS placeholders (@base_url_path, @locale)
- Passes HTML + CSS to Emogrifier (Pelago\Emogrifier\CssInliner)
- Emogrifier converts CSS selectors to inline style="" attributes
โ
6. Final inlined HTML sent via SMTP transport (Symfony Mailer)
Magento splits email CSS into two files because email clients like Gmail strip <style> tags:
| File | Purpose | Directive | Processing |
|---|---|---|---|
email-inline.css (from email-inline.less) |
Styles that CAN be inlined | {{inlinecss file="css/email-inline.css"}} |
Emogrifier converts to style="" attributes |
email.css (from email.less) |
Styles that CANNOT be inlined | {{css file="css/email.css"}} |
Placed in <style> tag (media queries, :hover, @font-face) |
The header template (header.html) contains both directives:
<style type="text/css">
{{var template_styles|raw}}
{{css file="css/email.css"}}
</style>
<!-- ... later in the template ... -->
{{inlinecss file="css/email-inline.css"}}
template_styles VariableEach email template can declare per-template styles in a <!--@styles @--> comment block at the top:
<!--@styles
.custom-class { color: #333; }
@-->
These are injected into the <style> tag via {{var template_styles|raw}}.
Hyvรค replaces the Luma/Blank LESS-based frontend with TailwindCSS. This breaks the LESS file inheritance chain that Magento's email system depends on. The hyva-themes/magento2-email-module re-adds the necessary LESS files and creates a fallback specifically for email rendering.
Module: Hyva_Email (enabled in app/etc/config.php)
Location: vendor/hyva-themes/magento2-email-module/src/
view/frontend/web directory to the design fallback so email.less and email-inline.less are found during static content deploymentLocated at vendor/hyva-themes/magento2-email-module/src/view/frontend/web/css/:
| File | Purpose |
|---|---|
email.less |
Master non-inline styles import file |
email-inline.less |
Master inline styles import file |
email-fonts.less |
@font-face declarations |
source/_email-base.less |
Core email stylesheet (resets, layout, typography, tables, buttons) |
source/_email-extend.less |
Theme customization file (extend without copying _email-base) |
source/_email-variables.less |
Variable overrides for email-specific values |
source/_variables.less |
Local theme variable overrides |
source/_theme.less |
Global theme variable overrides |
source/_typography.less |
@font-face rule generation |
@import 'source/lib/_lib.less'; // Global Magento UI library
@import 'source/lib/variables/_email.less'; // Global email variables
@import 'source/_theme.less'; // Global variable overrides
@import 'source/_variables.less'; // Local theme variables
@import 'source/_email-variables.less'; // Email-specific variables
@import 'source/_email-base.less'; // Core email styles
@import 'source/_email-extend.less'; // Theme customizations
//@magento_import 'source/_email.less'; // Module-specific email styles
_email-base.less contains all styles. The build splits them:
.email-non-inline() and .media-width() mixins โ email.css (in <style> tag)email-inline.css (inlined by Emogrifier)app/design/frontend/Uptactics/nto/
โโโ Magento_Email/
โ โโโ email/
โ โ โโโ header.html # Custom email header (wraps ALL emails)
โ โ โโโ footer.html # Custom email footer
โ โโโ web/
โ โโโ logo_email.png # Custom email logo
โโโ Magento_Sales/
โ โโโ email/
โ โโโ order_new.html # Custom new order email
โ โโโ order_new_guest.html # Custom new order for guests
โ โโโ invoice_new.html # Custom invoice email
โ โโโ shipment_new.html # Custom shipment email
โ โโโ creditmemo_new.html # Custom credit memo email
โโโ Magento_Customer/
โ โโโ email/
โ โโโ account_new.html # Custom new account email
โ โโโ password_new.html # Custom new password email
โ โโโ password_reset_confirmation.html
โโโ Magento_Contact/
โ โโโ email/
โ โโโ submitted_form.html # Custom contact form email
โโโ web/
โโโ css/
โโโ email.less # Override non-inline styles (optional)
โโโ email-inline.less # Override inline styles (optional)
โโโ source/
โโโ _email-extend.less # Custom style overrides (recommended)
โโโ _email-variables.less # Custom variable overrides (recommended)
โโโ _theme.less # Global variable overrides
email_template table) - highest priorityapp/design/frontend/Uptactics/nto/)vendor/hyva-themes/magento2-default-theme/)vendor/hyva-themes/magento2-email-module/src/)vendor/mage-os/module-*/view/frontend/email/)Copy the source file to your theme following this path convention:
vendor/mage-os/module-{name}/view/frontend/email/{filename}.html
โ
app/design/frontend/Uptactics/nto/Magento_{Name}/email/{filename}.html
For CSS overrides:
vendor/hyva-themes/magento2-email-module/src/view/frontend/web/css/source/_email-extend.less
โ
app/design/frontend/Uptactics/nto/web/css/source/_email-extend.less
| Template | Source Module | Source Path |
|---|---|---|
| Header | module-email |
view/frontend/email/header.html |
| Footer | module-email |
view/frontend/email/footer.html |
| New Order | module-sales |
view/frontend/email/order_new.html |
| New Order (Guest) | module-sales |
view/frontend/email/order_new_guest.html |
| Invoice | module-sales |
view/frontend/email/invoice_new.html |
| Shipment | module-sales |
view/frontend/email/shipment_new.html |
| Credit Memo | module-sales |
view/frontend/email/creditmemo_new.html |
| New Account | module-customer |
view/frontend/email/account_new.html |
| Password Reset | module-customer |
view/frontend/email/password_reset_confirmation.html |
| Contact Form | module-contact |
view/frontend/email/submitted_form.html |
| Newsletter Sub | module-newsletter |
view/frontend/email/subscr_success.html |
Create app/design/frontend/Uptactics/nto/web/css/source/_email-variables.less:
// Brand colors
@email__background-color: #f5f5f5;
@email-body__background-color: #ffffff;
@email-body__width: 600px;
// Links
@link__color: #006bb4;
@link__text-decoration: underline;
@link__visited__color: #006bb4;
// Header
@email-header__background-color: #003366;
// Buttons
@button__background-color: #006bb4;
@button__border-color: #006bb4;
@button__color: #ffffff;
// Typography
@font-family__base: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
@font-size__base: 14px;
@heading__color: #333333;
Create app/design/frontend/Uptactics/nto/web/css/source/_email-extend.less:
@import url("@{baseUrl}css/email-fonts.css");
// Custom header styles
.email-header {
background-color: @email-header__background-color;
}
// Custom button styles
.email-button {
border-radius: 4px;
text-transform: uppercase;
font-weight: bold;
}
// Custom footer
.email-footer {
border-top: 2px solid @border__color;
padding-top: 20px;
}
Use PostCSS to compile Tailwind @apply directives into LESS-compatible CSS.
Create app/design/frontend/Uptactics/nto/web/tailwind/emails/postcss.config.js:
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss/nesting'),
require('tailwindcss')({ config: './emails/tailwind.email.config.js' }),
]
}
Create app/design/frontend/Uptactics/nto/web/tailwind/emails/tailwind.email.config.js:
const defaultConfig = require('../tailwind.config.js');
module.exports = {
...defaultConfig,
corePlugins: {
// CRITICAL: Disable opacity plugins - LESS cannot parse RGBA syntax
backdropOpacity: false,
backgroundOpacity: false,
borderOpacity: false,
divideOpacity: false,
ringOpacity: false,
textOpacity: false
}
};
Create app/design/frontend/Uptactics/nto/web/tailwind/theme/email.css:
.footer {
@apply border-t-2 border-primary;
}
.email-header {
@apply bg-primary text-white;
}
.btn-primary {
@apply bg-primary text-white font-bold py-2 px-4 rounded;
}
Add build script to app/design/frontend/Uptactics/nto/web/tailwind/package.json:
{
"scripts": {
"build-email": "npx postcss --config ./emails theme/email.css -o ../css/source/_theme.less"
}
}
cd app/design/frontend/Uptactics/nto/web/tailwind
npm run build-email
This outputs web/css/source/_theme.less with plain CSS (no Tailwind utilities), which Magento's LESS processor can consume.
Known constraints:
border-b, border-t) may need explicit CSS fallbacksurl('@{baseDir}css/bg.svg')| Directive | Usage | Example |
|---|---|---|
{{var name}} |
Output escaped variable | {{var order.increment_id}} |
{{var name|raw}} |
Output unescaped HTML | {{var template_styles|raw}} |
{{var name|nl2br}} |
Newlines to <br> |
{{var comment|nl2br}} |
{{trans "text"}} |
Translatable string | {{trans "Thank you for your order."}} |
{{trans "text %var" var=$val}} |
Translated with variable | {{trans "Dear %name" name=$customer.name}} |
{{template config_path="..."}} |
Include configured template | {{template config_path="design/email/header_template"}} |
{{layout handle="..." ...}} |
Render layout block | {{layout handle="sales_email_order_items" order=$order}} |
{{css file="..."}} |
CSS in <style> tag |
{{css file="css/email.css"}} |
{{inlinecss file="..."}} |
CSS for Emogrifier inlining | {{inlinecss file="css/email-inline.css"}} |
{{depend condition}} |
Conditional block | {{depend store_phone}}...{{/depend}} |
{{if condition}} |
If/else branching | {{if order.getIsNotVirtual()}}...{{/if}} |
Every email template starts with metadata:
<!--@subject {{trans "Your %store_name order confirmation" store_name=$store.frontend_name}} @-->
<!--@vars {
"var order.increment_id":"Order ID",
"var order.created_at":"Order Date",
"var billing":"Billing Address HTML"
} @-->
<!--@styles
.custom-table { border: 1px solid #ccc; }
@-->
| Variable | Description |
|---|---|
$store |
Store object |
$store.frontend_name |
Store display name |
$store_email |
Support email address |
$store_phone |
Store phone number |
$store_hours |
Business hours |
$logo_url |
Email logo image URL |
$logo_alt |
Logo alt text |
$logo_width |
Logo width |
$logo_height |
Logo height |
$template_styles |
Per-template CSS styles |
email-inline.less compiled to email-inline.css{{inlinecss}} directive loads the compiled CSSstyle="" attributes#id).class)table, td, p)table td)table > tr)h1 + p)[attr], [attr=value]):hover, :first-child, :nth-child)::before, ::after)*)@media)@font-face declarations<table>, <tr>, <td>) - not <div> with CSSwidth on tables/images via HTML attribute AND CSScellpadding, cellspacing, border attributes on tablesposition, float, flexbox, gridalign attribute for centering (not margin: 0 auto alone)padding-top not padding shorthand#FFFFFF not #FFF.media-width() mixins (outputs to email.css)Place logo at:
app/design/frontend/Uptactics/nto/Magento_Email/web/logo_email.png
Navigate to: Content > Design > Configuration > [Store View] > Transactional Emails
Provide image at 3x display size. For 200x100px display, upload 600x300px image and set:
# Deploy static content (includes email CSS compilation)
ddev exec bin/magento setup:static-content:deploy -f --area=frontend --theme Uptactics/nto
# Or use composer script
ddev exec composer build-static
# Verify compiled output exists
ls -la pub/static/frontend/Uptactics/nto/en_US/css/email*.css
# Clear view preprocessed (required for LESS changes)
ddev exec rm -rf var/view_preprocessed/*
# Clear static content
ddev exec rm -rf pub/static/frontend/Uptactics/nto/*
# Flush Magento cache
ddev exec bin/magento cache:flush
# Redeploy static content
ddev exec bin/magento setup:static-content:deploy -f --area=frontend --theme Uptactics/nto
ddev exec rm -rf var/view_preprocessed/* pub/static/frontend/Uptactics/nto/*
ddev exec bin/magento cache:flush
ddev exec bin/magento setup:static-content:deploy -f --area=frontend --theme Uptactics/nto
IMPORTANT: Every email styling change MUST be visually verified via Mailpit before considering the change complete. This is a mandatory step in the workflow.
DDEV runs Mailpit internally to capture all outgoing emails. No emails leave the local environment.
https://ntotank.ddev.site:8443/mailpit/http://127.0.0.1:8025/mailpit/api/v1/All API calls use the base URL http://127.0.0.1:8025/mailpit/api/v1/.
| Endpoint | Method | Description |
|---|---|---|
/messages |
GET | List all messages (paginated). Returns total, count, messages[] |
/message/{ID} |
GET | Get full message metadata (From, To, Subject, HTML, Text, Size, etc.) |
/message/{ID}/html |
GET | Get rendered HTML body only (for visual inspection) |
/messages |
DELETE | Delete all messages |
/search?query={query} |
GET | Search messages by subject, from, to, or content |
After making any email template or CSS change, follow this process:
ddev exec rm -rf var/view_preprocessed/* pub/static/frontend/Uptactics/nto/*
ddev exec bin/magento cache:flush
ddev exec bin/magento setup:static-content:deploy -f --area=frontend --theme Uptactics/nto
Place a test order or trigger the relevant transactional email from the admin panel. For order emails, use the admin to create a test order or resend an existing order confirmation.
# List all captured emails
curl -s http://127.0.0.1:8025/mailpit/api/v1/messages | python3 -m json.tool
# Get the most recent email's ID and subject
curl -s http://127.0.0.1:8025/mailpit/api/v1/messages | python3 -c "
import sys, json
data = json.load(sys.stdin)
for msg in data['messages']:
print(f'ID: {msg[\"ID\"]}')
print(f'Subject: {msg[\"Subject\"]}')
print(f'From: {msg[\"From\"][\"Name\"]} <{msg[\"From\"][\"Address\"]}>')
print(f'To: {msg[\"To\"][0][\"Name\"]} <{msg[\"To\"][0][\"Address\"]}>')
print(f'Date: {msg[\"Created\"]}')
print('---')
"
# Get full message details (includes HTML, Text, metadata)
curl -s http://127.0.0.1:8025/mailpit/api/v1/message/{MESSAGE_ID} | python3 -m json.tool
# Get just the rendered HTML (for saving/viewing)
curl -s http://127.0.0.1:8025/mailpit/api/v1/message/{MESSAGE_ID}/html > /tmp/email_preview.html
Open Mailpit in the browser to visually confirm styling:
https://ntotank.ddev.site:8443/mailpit/
Click on the email to view the rendered HTML. Mailpit displays the email as it would appear in a mail client. Check:
To confirm Emogrifier is properly inlining styles, inspect the raw HTML:
# Check that inline style attributes are present on elements
curl -s http://127.0.0.1:8025/mailpit/api/v1/message/{MESSAGE_ID}/html | grep -o 'style="[^"]*"' | head -20
# Check for <style> tag content (non-inline styles like media queries)
curl -s http://127.0.0.1:8025/mailpit/api/v1/message/{MESSAGE_ID}/html | grep -oP '<style[^>]*>.*?</style>' | head -5
What to look for:
style="" attributes on <body>, <table>, <td>, <p>, <a>, <h1>-<h6> elements confirm inlining is working<style> tag should contain media queries and :hover/:visited/:active pseudo-class rulesAfter every email style change, confirm ALL of the following:
style attributes present on HTML elements (Emogrifier working)<style> tag (responsive styles)Navigate to: Marketing > Communications > Email Templates
Note: Admin preview does NOT process {{inlinecss}} or {{css}} directives. Mailpit shows the actual rendered email as sent, making it the definitive verification method.
For production readiness, additionally test on these priority clients:
<style> tags, inline onlyCustom email templates cannot call methods directly on objects. Only scalar values and DataObject getData() access are allowed.
{{var order.getCustomerName()}}
{{var subscriber.getConfirmationLink()}}
{{var order_data.customer_name}}
{{var subscriber_data.confirmation_link}}
ddev exec bin/magento dev:email:override-compatibility-check
ddev exec bin/magento dev:email:newsletter-compatibility-check
CSS inlining error: Compilation from source: LESS file is empty: frontend/.../css/email-inline.less
Cause: Hyvรค theme doesn't have LESS files in its inheritance chain.
Fix: Ensure Hyva_Email module is enabled:
ddev exec bin/magento module:status Hyva_Email
ddev exec bin/magento module:enable Hyva_Email
ddev exec bin/magento setup:upgrade
ddev exec rm -rf var/view_preprocessed/*ddev exec rm -rf pub/static/frontend/Uptactics/nto/*ddev exec bin/magento setup:static-content:deploy -f --area=frontendddev exec bin/magento cache:flushls pub/static/frontend/Uptactics/nto/en_US/css/email*.css*)email-inline.less, not email.lessMagento_{ModuleName}/email/{filename}.htmlddev exec bin/magento cache:flushvendor/mage-os/module-email/Model/AbstractTemplate.php - Template loading & processingvendor/mage-os/module-email/Model/Template/Filter.php - Directive processing (1,145 lines)vendor/mage-os/framework/Css/PreProcessor/Adapter/CssInliner.php - Emogrifier wrappervendor/hyva-themes/magento2-email-module/src/view/frontend/web/css/ - LESS source filesvendor/hyva-themes/magento2-email-module/src/Plugin/FallbackRulePlugin.php - Design fallbackvendor/hyva-themes/magento2-email-module/src/Plugin/PackageFilePlugin.php - Static deploymentvendor/mage-os/module-email/view/frontend/email/header.html - Email headervendor/mage-os/module-email/view/frontend/email/footer.html - Email footervendor/mage-os/module-sales/view/frontend/email/ - All sales email templates (16 templates)vendor/mage-os/module-customer/view/frontend/email/ - Customer email templatesvendor/mage-os/module-contact/view/frontend/email/ - Contact form templatevendor/mage-os/module-email/etc/email_templates.xml - Header/footer template IDsvendor/mage-os/module-sales/etc/email_templates.xml - Sales template IDs (16 templates)vendor/mage-os/module-customer/etc/email_templates.xml - Customer template IDspub/static/frontend/Uptactics/nto/en_US/css/email.css - Non-inline stylespub/static/frontend/Uptactics/nto/en_US/css/email-inline.css - Inline stylespub/static/frontend/Uptactics/nto/en_US/css/email-fonts.css - Font declarations