@clay-design-system/email
The Lifecycle / Email tier of the Clay Design System — responsive, Outlook-safe HTML emails authored in MJML and composed from the shared Clay foundation tokens.
Why MJML + inlined tokens
Email clients (notably Outlook) do not support CSS custom properties (var(--clay-*)). So this
package never ships var() — instead a build step reads the resolved literal token values from
@clay-design-system/tokens (the dist/tokens.js ES export) and inlines them into MJML at build time.
@clay-design-system/tokens (JS literals)
↓ src/tokens/build-email-tokens.ts (generates)
src/generated/base-head.mjml ← <mj-head>: fonts, mj-class roles, dark-mode
src/generated/email-tokens.ts ← inline-able constants + px() helper
↓ MJML sections + container
src/build/compile.ts (mjml2html)
↓
dist/emails/<name>.html ← standalone, previewable
dist/emails/<name>.braze.html ← Braze full-template variant (unsubscribe token)
dist/emails/<name>.static.html ← hand-authored fallback (no MJML)
dist/catalog.json ← Storybook + docs source
Structure
src/container/— the shared email shell (page bg → 640px card → 560px inner, preheader, responsive + dark/light). Every email reuses it.src/sections/— MJML section partials (logo-header, hero, feature-rows, icon-feature-list, buttons-primary, footer).src/emails/— full email compositions (container + sections) plus a static-HTML fallback. Gitignored / local-only: every composed email is a generation, so it stays on the machine that made it and contributors never see each other's campaigns. The build globs this folder at compile time, so generating into it works unchanged — it is simply never committed. A fresh clone starts empty here;src/examples/holds the committed Email Love → Braze reference exports.src/tokens/— the token→email bridge codegen.src/build/— MJML compile + Braze transform + catalog.
Scripts
pnpm --filter @clay-design-system/email build # tokens bridge → tsc → compile MJML
pnpm --filter @clay-design-system/email dev # watch + recompile MJML
pnpm --filter @clay-design-system/email tokens:check # fail if generated token artifacts drift
pnpm --filter @clay-design-system/email test # vitest
What the npm package ships
Only the public surface: dist/index.js + types (the inline token constants), @clay-design-system/email/catalog.json, …/emails/* (compiled HTML,
Braze and static variants). Source maps, dist/build, dist/examples and dist/exports (the Braze campaign exports with their
images, ~11 MB) stay in the repo: they are build output, not importable through the exports map. package.json files is an
allowlist, so new build output is not published until it is added there.
Documented in Storybook under Lifecycle/Email/.