用 React Email 写出“在邮箱里不垮”的邮件模板:Novu 场景下的完整样式指南
2026/9/10 10:38:47 网站建设 项目流程

用 React Email 写出“在邮箱里不垮”的邮件模板:Novu 场景下的完整样式指南

【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu

邮件是少数仍停留在二十年前渲染技术的渠道:没有标准化的 CSS 支持,客户端各自为政。用 React Email(以 React 组件的方式编写 HTML 邮件)时,样式怎么写、用不用 Tailwind、为什么不能用rem,这些问题的答案都直接决定模板在 Gmail、Outlook、Apple Mail 中是否正常显示。本文以 Novu 开源仓库内置的react-email技能文档 STYLING.md 为核心,系统讲解 React Email 模板的样式规范、客户端限制规避技巧、品牌一致性方案,并结合仓库中真实的邮件模板(如 Novu onboarding 邮件、代码优先框架的 email step)给出可落地的写法。

先厘清上下文:这份样式指南用在哪

在进入细节前,先交代这份文档所处的环境。Novu 仓库根目录下的.agents/skills/react-email/是一套面向“生成/维护 HTML 邮件模板”场景的完整技能(skill),由 SKILL.md 定义入口,配套references/目录下的四份参考文档分工明确:

  • COMPONENTS.md:组件总览;
  • STYLING.md:样式规范(本文核心);
  • PATTERNS.md:常见模板范式;
  • SENDING.md:投递相关。

在 Novu 生态里,React Email 有着真实且具体的落点:Novu 的“代码优先框架(Code-First Framework)”允许开发者在 workflow 中用step.email()渲染邮件,而邮件正文正是由 React Email 组件编译而来。仓库文档 react-email.mdx 给出了完整的接入三步走:先安装@react-email/components,再编写模板组件并导出renderEmail(),最后在 workflow 的 email step 中把渲染结果赋给body。同时novuCLI 还内置了app-react-email脚手架模板,其中有一封完整的 onboarding 邮件可供对照学习。这意味着下面每一类样式规则,都能在 Novu 仓库里找到真实范例。

选对样式入口:Tailwind组件 vs 行内样式

React Email 的样式方案核心决策只有一条(见 STYLING.md):

如果项目在使用 Tailwind CSS,就用<Tailwind>组件包裹模板并以 className 编写样式;否则退化为行内样式。

推荐写法的示例代码如下:

import { Tailwind, pixelBasedPreset } from '@react-email/components'; <Tailwind config={{ presets: [pixelBasedPreset], theme: { extend: { colors: { brand: '#007bff', }, }, }, }} > {/* Email content */} </Tailwind>

<Tailwind>本质上会在渲染阶段把模板中的 Tailwind 工具类编译成邮件客户端可识别的内联/嵌入 CSS。为了模板之间风格一致,还应在设计阶段把品牌色等 token 注入theme.extend,供bg-brandtext-brand这类语义类名使用。

在 Novu 仓库中,app-react-email脚手架的 onboarding 邮件正是这一做法的真实范例:novu-onboarding-email.tsx 中<Tailwind>theme.extend.colors定义了brand: '#2250f4'offwhite: '#fafbfb'blurwhite: '#f3f3f5'三个语义色,并自定义了像素级spacing,随后<Body className="bg-blurwhite text-base font-sans">直接消费这些 token——这就是“品牌配置集中、模板只写语义类名”的工程化形态。

pixelBasedPreset:为什么必须配它

样式指南特别强调了一个很多 React 开发者会踩的坑:邮件客户端不支持rem单位。Tailwind 默认的间距、字号体系(p-4text-base等)底层换算基于rem,在邮件里会失效或被错误渲染。

pixelBasedPreset就是为此准备的——它是@react-email/components导出的一个 Tailwind 预设(preset),作用是在编译阶段把基于rem的工具类换算成像素值:

import { pixelBasedPreset } from '@react-email/components'; <Tailwind config={{ presets: [pixelBasedPreset] }}>

因此规则是:只要用<Tailwind>就应始终把pixelBasedPreset放进config.presets。可以看到 SKILL.md 中的每个示例模板都遵循了这一约定。仓库里docs/framework/content/react-email.mdxI18N.md配套的示例(例如 next-intl 本地化的邮件模板)同样一律携带config={{ presets: [pixelBasedPreset] }},侧面印证了这是不可省略的标准配置。

邮件客户端的 CSS 边界:先知道“不能做什么”

在写任何样式之前,必须先背下邮件客户端的硬性限制清单(STYLING.md)。这些不是“建议”,而是 Gmail、Outlook、Apple Mail、Yahoo Mail 渲染引擎的客观事实:

特性结论替代方案
SVG / WEBP 图片不支持,可能直接不显示只用 PNG / JPEG
Flexbox / Grid 布局大部分客户端不识别Row/Column组件或原生 table
媒体查询(sm:md:lg:xl:多数桌面客户端忽略移动优先的堆叠式布局
主题选择器(dark:light:不支持需要深色版本时用固定背景色
rem单位不被支持pixelBasedPreset转像素

与之呼应的是,@react-email/components提供RowColumnSection等组件,它们在底层全部编译为<table>/<td>,这正是“邮件里没有 flexbox,只有表格”这一事实的体现。样式指南同步建议:不要试图在邮件里用 CSS 媒体查询做响应式,而是默认按移动端设计(详见后文 Layout 小节)。

边框(Border)的三条铁律

邮件客户端对border的简写处理尤其脆弱。如果不显式指定 border-style,很多客户端根本不渲染边框。样式指南给出了正确与错误的对比(STYLING.md):

// 正确 —— 显式声明 border style <div className="border-solid border border-gray-300" /> // 正确 —— 只画单侧边框时,先清掉其余三侧 <div className="border-none border-l border-solid border-l-gray-300" /> // 错误 —— 缺少 border-style,邮件客户端可能不显示 <div className="border border-gray-300" />

记住这一口诀:凡是写border,永远带上border-solid/border-dashed之类的 style;只画单侧时先border-none再补那一侧。这也是 SKILL.md 中“Always specify border type”“单侧边框记得重置其余边框”两条行为准则的直接来源。

组件结构规范:<Head />与 PreviewProps

<Head />必须放在<Tailwind />内部

Tailwind 生成的样式需要被Head注入。若HeadTailwind外层,生成的<style>将无法正确内联。标准骨架如下(STYLING.md):

<Html> <Tailwind config={{ presets: [pixelBasedPreset] }}> <Head /> <Body>...</Body> </Tailwind> </Html>

真实范例见 novu-onboarding-email.tsx:组件外层是<Html>,内部<Head />紧随<Preview><Tailwind>则包住<Head /><Body />,顺序与规范完全一致。

PreviewProps:只放组件真正用到的 props

.PreviewProps是静态属性,供开发预览与测试时为模板提供示例数据。规范要求只包含组件实际消费的 props,避免把无用数据带进预览:

const Email = ({ source }: { source: string }) => { return ( <div> <a href={source}>Click here</a> </div> ); }; Email.PreviewProps = { source: "https://example.com", };

若为 Novu 动态消息编写模板,Props 来源通常是 workflow 里定义的 controls/payload schema。app-react-email脚手架中用type NovuWelcomeEmailProps = ControlSchema & PayloadSchema声明组件 props(见 novu-onboarding-email.tsx),ControlSchemaPayloadSchema从相邻的 workflow 定义导入,随后通过renderEmail(controls, payload)将二者合并传给模板。这相当于把 PreviewProps 的“单一数据来源”思想扩展到了生产渲染链路。

默认布局结构:从 Body 到 Footer

为保证观感一致,指南规定了一套默认布局模板(STYLING.md):

  • Body:内容底色与整体排版基调,例如className="font-sans py-10 bg-gray-100"
  • Container:白色背景、水平居中、内容左对齐的卡片,例如className="mx-auto bg-white p-6 rounded"
  • Footer:必须包含物理地址、退订链接、当年份,例如:
<Section className="text-center text-gray-500 text-sm"> <Text className="m-0">123 Main St, City, State 12345</Text> <Text className="m-0">&copy; {new Date().getFullYear()} Company Name</Text> <Link href={unsubscribeUrl}>Unsubscribe</Link> </Section>

注意 Footer 中地址与版权行都加了m-0,以消除列表项/段落间的默认外边距。这在项目模板里同样可见——Novu onboarding 邮件的页脚 Container 使用<Container className="mt-20">+<Text className="text-center text-gray-400 mb-45">承载“Powered by Novu”声明(novu-onboarding-email.tsx)。

排版:用字号与间距拉开信息层级

排版规范(STYLING.md)遵循一个朴素的视觉层级原则:标题要“重”,正文要“轻”;标题外边距大,段落外边距小

// 标题:加粗、更大字号、更大外边距 <Heading className="text-2xl font-bold text-gray-900 mb-4"> // 段落:常规字重、较小字号、较小外边距 <Text className="text-base text-gray-700 mb-3">

规范进一步要求整套模板使用“尊重内容层级的一致间距”,避免每个模板各自为政。在 Novu 的 onboarding 模板里可以看到统一间距 token 的做法:spacing被显式覆盖为0: '0px'20: '20px'45: '45px'(见 novu-onboarding-email.tsx),my-20mb-45p-45等都落在同一套像素级间距体系内,这就是“风格一致可维护”的工程化落地。

图片规范:只有 PNG / JPEG,必须绝对 URL

图片是邮件兼容问题的重灾区,样式指南列出的规则(STYLING.md)如下:

  • 只有用户明确提出才放入图片;
  • 正文内容图使用响应式尺寸(w-fullh-auto);
  • 24–48px 的小图标允许固定尺寸;
  • 绝不拉伸变形用户提供的图片;
  • 绝不自行创建 SVG;
  • 一律使用绝对 URL(本地开发时依赖 dev server 的/static/静态目录);
  • 必须带alt文本保证可访问性。
<Img src="https://example.com/image.png" alt="Description" className="w-full h-auto" />

静态资源该放哪

关于图片与字体的存放位置,规范(STYLING.md)给出两条明确路径:

  • Logo / 内容图片:托管在 CDN 或公网 URL;本地开发时放入emails/static/目录,由 dev server 提供;
  • 自定义字体:通过Font组件引用网络字体 URL(Google Fonts、Adobe Fonts 或自托管字体)。

实践上还要区分开发/生产 URL。Novu 官方模板的做法具有参考价值:onboarding 模板直接使用托管在图片服务上的绝对地址,并为Img标注了widthheightalt="Novu"(novu-onboarding-email.tsx),头像类图片则额外追加rounded-full圆角样式,属典型的“绝对 URL + 尺寸 + alt”三重保险。

按钮:永远带上box-border

按钮在多数邮件客户端中按border-box以外的盒模型计算,导致 padding 溢出撑破布局。因此规范(STYLING.md)要求按钮类名里必须包含box-border,配合block(占满可用宽度)与text-center no-underline(去下划线)让整块区域可点击:

<Button href="https://example.com" className="bg-blue-600 text-white px-5 py-3 rounded box-border block text-center no-underline" > Click Here </Button>

对应的实现事实是:React Email 的Button在邮件中最终渲染为一个带display: inline-block样式包裹<a>的表格式按钮。仓库模板中的 CTA 同样遵循“整块点击”思路,例如 onboarding 邮件内的按钮用bg-[#000000] rounded text-white ... no-underline text-center px-5 py-3(novu-onboarding-email.tsx)。

布局:移动优先 + 表格式多列

邮件没有“响应式断点”可用,因此默认就按移动端设计(STYLING.md):

  • 优先堆叠式布局(任何屏幕宽度都成立);
  • 主容器最大宽度约 600px;
  • 移除列表项之间的默认间距/margin/padding。

需要多列排版时,用Row/Column代替 flexbox/grid:

<Row> <Column className="w-1/2">Left content</Column> <Column className="w-1/2">Right content</Column> </Row>

Novu onboarding 模板里的“用户邀请”区块就很有代表性:先用<Row align={...}>建立一行,再用三个<Column>分别承载头像、箭头、团队头像,并各自通过align="right/center/left"控制对齐(novu-onboarding-email.tsx),是“表格列布局替代 flexbox”的教科书式示范。

深色模式:客户端不支持选择器,就手动配色

由于dark:/light:主题选择器在邮件客户端里不工作,当用户明确要求深色邮件时,规范(STYLING.md)给出了两套固定的配色约定:

  • 内容卡片:纯黑#000
  • 页面背景:深灰#151516
<Body className="bg-[#151516]"> <Container className="bg-black text-white">

本质上是把“深色模式”当成一套独立模板做静态配色,而不是运行时切换主题。

品牌一致性:建一个集中的 Tailwind 配置

动手前先收集品牌色

规范建议在制作邮件前,先向用户收集一套完整品牌色板(STYLING.md),并给出了每种颜色的默认建议值:

颜色角色用途建议默认值
Primary按钮、链接、关键强调用户品牌主色
Secondary边框、背景、次要元素用户品牌辅助色
Text正文文本色#1a1a1a(浅色背景)
Text muted说明文字、页脚#6b7280
Background邮件整体背景#f4f4f5
Surface容器/卡片背景#ffffff

文档还附带了一段可直接发给用户的“品牌信息收集 prompt”,引导其一次性提供主色 hex、可公开访问的 PNG/JPEG logo、辅助色与风格偏好,避免来回确认。

集中式tailwind.config.ts

为了在多个模板间保持品牌一致,规范要求把所有品牌 token 收敛到一个中央配置文件,供全部邮件模板 import(STYLING.md)。使用satisfies TailwindConfig可以让 IDE 对全部配置项提供智能提示:

// emails/tailwind.config.ts import { pixelBasedPreset, type TailwindConfig } from '@react-email/components'; export default { presets: [pixelBasedPreset], theme: { extend: { colors: { brand: { primary: '#007bff', secondary: '#6c757d', }, }, }, }, } satisfies TailwindConfig; // 非 Tailwind 的品牌素材(可选) export const brandAssets = { logo: { src: 'https://example.com/logo.png', alt: 'Company Name', width: 120, }, };

各模板只需引入共享配置与品牌素材:

import tailwindConfig, { brandAssets } from './tailwind.config'; <Tailwind config={tailwindConfig}> <Body className="bg-gray-100 font-sans"> <Container className="bg-white p-6"> <Img src={brandAssets.logo.src} alt={brandAssets.logo.alt} width={brandAssets.logo.width} /> <Button className="bg-brand-primary text-white">Action</Button> </Container> </Body> </Tailwind>

一致性维护四条守则

配套的长期维护规则(STYLING.md):

  1. 永远使用品牌配置——任何模板不得硬编码颜色;
  2. 改配置、不改模板——品牌换色只更新tailwind.config.ts一处;
  3. 使用语义化命名——写bg-brand-primary,不写bg-[#007bff]
  4. 保证对比度——正文与背景的对比度至少满足 WCAG AA(4.5:1)。

第 3 点在文案上最容易被忽略:字面量色值bg-[#007bff]一旦散落多封模板,改品牌色就变成全仓搜索替换;而语义名把“变化点”收敛到配置文件,正是上述 4 条守则共同指向的目标。

收尾检查清单:跨客户端与体积

样式写完后,需要按以下清单过一遍(STYLING.md):

  1. 模板要独一无二——针对用户具体场景定制,而非套用通用样板;
  2. 跨客户端测试——Gmail、Outlook、Apple Mail、Yahoo Mail 逐一验证(可用 Litmus / Email on Acid 做像素级校验,也可用 React Email 预览工具检查具体特性支持情况);
  3. 控制体积在 102KB 以内——Gmail 会对更大的邮件做截断(clipping);
  4. 关键词策略——在正文合理布局关键词以提升互动率;
  5. 行内样式兜底——部分客户端会剥掉<style>标签,必要时保留行内样式作为降级方案。

在 Novu 代码优先框架中串联这套规范

最后把这套样式规范放回 Novu 的真实工作流中串一遍。完整的推荐链路是:

  1. 编写模板:按本文规范用<Tailwind config={{ presets: [pixelBasedPreset] }}>包裹<Head /><Body />,品牌色统一来自中央tailwind.config.ts
  2. 导出渲染函数:在模板文件底部导出renderEmail(),如 novu-onboarding-email.tsx 中export function renderEmail(controls, payload) { return render(<NovuWelcomeEmail ... />); }
  3. 接入 workflow:在 Novu 代码优先框架的 workflow 中通过step.email('send-email', ...)返回{ subject, body: renderEmail(...) },body 即最终发送的 HTML——完整示例见 react-email.mdx;
  4. 样板参考:需要从零搭建时,可参考 CLI 脚手架app-react-email模板的目录结构与写法(packages/novu/src/commands/init/templates/app-react-email/),其中的 onboarding 邮件几乎覆盖了本文提到的全部样式要点:集中色板、像素级间距、表格式多列、绝对 URL 图片、Footer 兜底。

同时建议配合阅读同技能目录下的 COMPONENTS.md(组件属性与用法)、PATTERNS.md(密码重置、订单确认等完整范式)以及 TESTS.md,形成“组件选型 → 样式落地 → 范式复用 → 测试验证”的完整闭环。

【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询