Email
@grundtone/email provides framework-agnostic, token-themed email building blocks. You author emails with MJML blocks that read grundtone design tokens; the build step compiles them to bulletproof, responsive, CSS-inlined HTML with {{handlebars}} placeholders left intact for a send-time templating layer, plus a plain-text fallback.
The only coupling between grundtone and the email artifact is token values from @grundtone/core — there is no Vue or React in the output. Email is just HTML + inlined CSS, themed by tokens, so grundtone stays multi-framework.
@grundtone/core (tokens)
│ resolveEmailTheme()
▼
blocks (token-themed MJML) → compile (MJML → inlined HTML) → artifact (HTML + text, {{vars}})
│
published to CDN ─────────────┤── notifications service fills vars, sends via Resend
(same pipeline as design tokens)Install
pnpm add @grundtone/emailQuick start
import {
resolveEmailTheme,
createBlocks,
baseLayout,
compileMjml,
toPlainText,
} from '@grundtone/email';
const theme = resolveEmailTheme();
const b = createBlocks(theme);
const mjml = baseLayout({
theme,
preheader: 'Your sign-in link is ready.',
content: [
b.heading({ text: 'Sign in' }),
b.text({ text: 'Click below to sign in.', size: 'lead' }),
b.button({ label: 'Sign in', href: '{{url}}' }),
].join('\n'),
});
const { html } = compileMjml(mjml, { validationLevel: 'strict' });
const text = toPlainText(html); // plain-text fallback, {{url}} preservedhtml is bulletproof inlined HTML that still contains {{url}}. A consumer fills the placeholders at send time.
Theming
resolveEmailTheme(theme?, options?) projects a grundtone Theme into the EmailTheme that blocks read from. With no argument it uses the default light preset. To brand an email, pass a resolved theme from createTheme:
import { createTheme } from '@grundtone/core';
const branded = createTheme({ light: { primary: '#996600' } });
const theme = resolveEmailTheme(branded.light);Lengths are converted from the token rem scale to px (Outlook is unreliable with rem); the values still come from tokens. Web fonts default to IBM Plex Sans via <mj-font>, with the token font stack as the fallback for clients that block web fonts.
GDPR
The default IBM Plex Sans web font is loaded from Google Fonts — email clients that fetch it expose the recipient's IP to Google. For GDPR-strict deployments, pass a self-hosted alternative via webFonts: [...] or disable web fonts entirely with webFonts: [] (the fallback stack still covers every client).
See Blocks for the building blocks and Templates for authoring, the send-time variable contract, and the CDN publish pipeline.
Security note
renderTemplate HTML-escapes recipient data, which is safe in HTML text and quoted attribute contexts. Escaping does not validate URL schemes — a value placed in href="{{url}}" is emitted verbatim, so javascript:/data: URLs survive. Pass trusted/validated URLs for link placeholders; URL-scheme allowlisting belongs at the data boundary (the built-in templates use system-generated URLs).
