Creating Custom Email Components
Email components are regular Hyvä CMS components with extra constraints: they must render email-safe HTML, keep Magento directives intact, and scope themselves to the email editors. This guide covers the email-specific rules - component declaration basics are in Creating Components.
Declaring an Email Component
Declare your component in your module's etc/hyva_cms/components.json, exactly like a CMS component, with one addition: the email context flag.
{
"vendor_promo_banner": {
"label": "Promo Banner",
"category": "Email",
"context_flags": ["email"],
"template": "Vendor_Module::elements/email/promo_banner.phtml",
"content": {
"text": {
"type": "richtext",
"label": "Banner Text",
"translate": true,
"default_value": "Free shipping on orders over $50"
}
},
"design": {
"background_color": {
"type": "color",
"label": "Background Color",
"default_value": "#eff6ff"
}
}
}
}
Scoping Your Component to Emails
"context_flags": ["email"] does two things:
- Your component appears in the picker inside email and newsletter templates (both editors declare the
emailcontext). - Your component is hidden everywhere else - CMS pages, blocks, menus, attributes.
The email editors also run in strict context mode (strict_component_context), so the reverse holds too: components without the email flag are never offered inside an email, even in nested containers. If your component does not carry the flag, it simply will not show up in email editors.
To make your component insertable inside the bundled Section, Two Columns, or Card containers, it must be listed in their accepts array - those containers declare an explicit child list. Override the bundled component declaration to extend accepts (see Overriding Existing Components), or design your component to sit directly inside the Email Wrapper, which accepts any email-flagged component.
Writing the Element Template
Email HTML lives by different rules than storefront HTML. The bundled components follow these conventions - match them and your component will pass the editor's health checks:
<?php
use Hyva\EmailTemplates\ViewModel\Utilities;
use Hyva\Theme\Model\ViewModelRegistry;
/** @var \Hyva\CmsLiveviewEditor\Block\Element $block */
/** @var \Magento\Framework\Escaper $escaper */
/** @var ViewModelRegistry $viewModels */
$emailUtility = $viewModels->require(Utilities::class);
// Richtext field: flatten to inline HTML so markup and {{var}}/{{trans}}
// directives survive unescaped. Falls back to the declared default.
$text = $emailUtility->inlineRichtext((string)($block->getData('text') ?: ''))
?: 'Free shipping on orders over $50';
$styles = $block->buildStyles([
'background-color' => $block->getData('background_color') ?: '#eff6ff',
'padding' => '12px 16px',
]);
?>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td align="center" style="<?= /** @noEscape */ $styles ?>">
<?= /** @noEscape */ $text ?>
</td>
</tr>
</table>
The rules, one by one:
Layout with tables, marked presentational. Use <table role="presentation"> for layout - Outlook cannot handle flex or grid, and without the role screen readers announce phantom rows and columns. Only genuine data tables (like Order Items) keep table semantics, with <th scope="col"> headers instead.
Inline-friendly CSS only. Emit per-instance styles through $block->buildStyles([...]) into style="" attributes, or use a <style> block with simple selectors - the InlineStyleProcessor inlines those at send time because Gmail strips <style> blocks. Avoid media queries, web fonts, and background images unless you accept degraded rendering in some clients.
Keep directives raw. Richtext and text field values may contain {{var ...}} and {{trans ...}} directives. Run richtext through Utilities::inlineRichtext() (strips the <p>/<div> wrappers, converts paragraph breaks to <br />) and output with /** @noEscape */. The directives resolve later - the Magento email filter in production, the dummy-data substitutor in preview. Escaping them would send literal {{var ...}} text to customers.
Meet WCAG AA contrast. Text needs a 4.5:1 contrast ratio against its background (3:1 for large text). The bundled palette uses #374151 for body text, #111827 for emphasis, and #4b5563 for muted text - all comfortably passing on white and #f9fafb. The editor's health check flags violations, including ones caused by merchant color choices.
Branch on preview when needed. $block->validPreview() is true in the editor preview. Use it when production output is a directive that only works in a real send:
<?php if ($block->validPreview()): ?>
<!-- dummy rows so the merchant sees something -->
<?php else: ?>
{{layout handle="sales_email_order_shipment_track" shipment_id=$shipment_id order_id=$order_id}}
<?php endif; ?>
Outlook buttons need VML. If you build clickable buttons, copy the pattern from elements/email/button.phtml - an anchor for modern clients wrapped in a VML roundrect fallback for Outlook. Note the VML branch uses strip_tags() on the label: VML cannot render HTML markup.
Giving a Field the Variable Picker
The Insert Variable picker is part of the richtext toolbar, so a field only gets variable-picker support when its type is richtext. Plain text fields have no picker (directives typed into them still work at render time - the picker is pure convenience). This is why the bundled directive-bearing fields (button labels, the Order Intro greeting) are all richtext.
Making Preview Variables Resolve
If your component emits a directive the preview does not know, the preview shows … in its place. Dummy values live in Model\DummyData\VariableSubstitutor - the variable map covers order_data.*, order.*, store.*, subscriber.*, and subscriber_data.* paths. For custom variables, add a plugin on the substitutor or handle the preview case in your template with validPreview().
Overriding Bundled Templates
Every element template can be overridden in a theme the standard Magento way, for example:
The same goes for the email chrome (root.phtml, header/default.phtml, footer/default.phtml) and the newsletter chrome (newsletter/header/default.phtml, newsletter/footer/default.phtml). Shared static CSS lives in web/css/hyva-email.css.
Related Topics
- Architecture - How component trees become sent emails
- Creating Components - The general component system
- Component Declaration Schema Reference - Every components.json key
- Previewing and Testing - The health checks your component should pass