Skip to content

HTML email

Your transactional email can match your app’s Hypermedia Components theme — including a custom theme built in the theme builder — without any runtime dependency. The kit generates theme-baked template fragments: token references are resolved to literal values (rem converted to px) and written into inline style attributes, because email clients don’t load external CSS and strip var() / custom properties.

  • Inline styles are the baseline. Colors, backgrounds, padding and typography are inlined per element — they survive every client, including forwards and the Gmail app reading non-Google (IMAP/POP) accounts, which drops <head> styles entirely.
  • One embedded <style> partial is enhancement-only: mobile container width and prefers-color-scheme: dark overrides via hc-em-* classes. Apple Mail and Outlook macOS honor it; Gmail ignores it and applies its own auto-invert. Every rule is safe to lose.
  • Table layouts, 600px container, role="presentation" skeletons, bulletproof buttons. No VML: Outlook’s Word engine renders square corners instead of border-radius — accepted degradation.
  • Generated files start with a manifest comment recording the core version, axis settings and the command/settings that produced them, so they can be regenerated after a theme change.

For the built-in axes there is nothing to generate: the core package ships baked artifacts under @hypermedia-components/core/email-artifacts/… — one theme per neutral ramp (default accent), both flavors:

email-artifacts/contract.json machine-readable contract
email-artifacts/default-<neutral>/email-tokens.json
email-artifacts/default-<neutral>/<flavor>/hc-email.html
email-artifacts/default-<neutral>/<flavor>/hc-email-layout.html

(<neutral>: gray, slate, zinc, neutral, stone; <flavor>: thymeleaf, plain.) Resolve them straight from the package (or a WebJar) instead of checking generated files into your repo — no regen ritual, and the artifacts always match the core version you depend on. contract.json lists every fragment’s th:fragment name and parameter list, so a downstream guard can validate against data instead of regexing the HTML:

Terminal window
node -e "console.log(require('@hypermedia-components/core/email-artifacts/contract.json').fragments.map(f => f.name + '(' + f.params + ')').join('\n'))"

Custom accents, custom tokens, or theme-builder exports still go through the flows below.

Open the theme builder, set up your theme (a built-in accent, a full custom theme — anything), and switch to the Email templates tab:

  • hc-email.html — all fragments (buttons, headings, text, link, separator, badges, alerts, panel, key-value table, footer).
  • hc-email-layout.html — the document shell (preheader, centered 600px card) with the enhancement <style> partial baked into <head>.
  • email-tokens.json — the resolved light/dark token values, for the runtime-theming escape hatch below.

Two flavors: Thymeleaf (fragments carry th:fragment signatures and th:text/th:href slots) and plain HTML (same markup with the Thymeleaf attributes stripped — re-add your own engine’s slot syntax).

The same three files come out of @hypermedia-components/cli, for scripted or CI regeneration:

Terminal window
npx @hypermedia-components/cli email eject --color indigo --neutral slate
npx @hypermedia-components/cli email eject --tokens my-theme.json --flavor plain
npx @hypermedia-components/cli email list # fragment inventory

--tokens accepts the theme builder’s DTCG export (accent tree or full-theme patch), which closes the loop: build the theme visually once, then regenerate the email templates reproducibly whenever tokens change. Files land in <dir>/email/; existing files are never overwritten without --force.

Drop the two files into src/main/resources/templates/email/. Each fragment is a parameterized th:fragment; escaping is handled by th:text. A mail template composes them:

templates/email/order-confirmed.html
<div th:fragment="content">
<div th:replace="~{email/hc-email :: hcHeading('ご注文ありがとうございます')}"></div>
<div th:replace="~{email/hc-email :: hcText(${customerName} + ' 様、ご注文が確定しました。')}"></div>
<div th:replace="~{email/hc-email :: hcKvTable(${orderSummary})}"></div>
<div th:replace="~{email/hc-email :: hcButton(${orderUrl}, '注文を確認する')}"></div>
<div th:replace="~{email/hc-email :: hcFooter('このメールに心当たりがない場合は破棄してください。')}"></div>
</div>

Wrap it in the layout and send the processed HTML with MimeMessageHelper#setText(html, true):

<div th:replace="~{email/hc-email-layout :: hcLayout(
'注文確認', 'ご注文が確定しました',
~{email/order-confirmed :: content})}"></div>

The available fragments and their parameters are documented per fragment in the package sources (@hypermedia-components/core/email/<name>/contract.md); the general patterns match the Thymeleaf guide.

Download the plain HTML flavor: the same baked markup with visible placeholder copy where the slots are. Copy a fragment into your engine (Freemarker, ERB, Razor, …), replace the placeholder text/URLs with your engine’s interpolation — and escape those interpolations yourself; without Thymeleaf, nothing does it for you.

Baked values assume the theme is fixed at generation time. If tenants have different themes at request time, load email-tokens.json per tenant and write the inline styles from it:

<td th:style="'background-color:' + ${t['button-primary-bg']} + ';border-radius:6px;'">

This is verbose by design — use it only for the tenant-variable subset (usually the accent on the button) and keep the rest baked.

The templates are generated artifacts: after changing your theme, download them again (the manifest comment at the top of each file tells you — or a teammate — exactly what produced it). Treat them like hc.tokens.css: commit them, don’t hand-edit them.