Text Color in HTML Email
Inline color when stylesheets do not survive. Hex, Outlook, and contrast on tinted cells.
The QA screenshot arrived with a subject line that said everything: “Why is our footer gray on gray in Outlook?” Marketing had approved the design in Figma. The ESP preview looked correct. Gmail on iPhone looked correct. But the footer in Outlook 365 on Windows showed #64748b body copy sitting on a #f1f5f9 cell that nobody remembered setting—because the color lived on a wrapper <div> that Word’s HTML engine never painted, and the paragraph inherited browser defaults instead of the token the team thought they shipped.
Email text color is not a stylesheet problem you solve once. It is a per-element inline problem you solve on every <p>, <h1>, <span>, and <td> that needs a hue different from the client default. The MDN color property documents the full CSS surface area; Can I email tells you which slice of that surface actually reaches inboxes. The gap between those two pages is where most production bugs live. If a human must read it, put color inline on that element.
Inline color as the delivery mechanism
Email clients do not share a rendering engine. Gmail strips <style> blocks in many contexts and inlines only what you put on the element. Outlook desktop uses Microsoft Word as its layout engine, which means modern color syntax, cascade behavior, and selector support diverge sharply from Chrome or Safari. Apple Mail on macOS and iOS is comparatively permissive, yet you still cannot assume a <style> rule targeting .body-copy will survive forwarding, “View original,” or a recipient who toggles remote content off and back on.
A <style> block can hold resets and media queries, but the values that define brand voice—headline navy, muted caption slate, error red—belong on the tags themselves. Email developers learn to batch properties on one style attribute because some clients discard duplicate tags or merge attributes unpredictably.
<p style="color:#334155;font-family:Georgia,'Times New Roman',serif;font-size:16px;line-height:24px;margin:0 0 16px;">
Your invoice for March is ready. Payment is due April 15.
</p>
Notice the stack: color, then font-family, then font-size, then line-height, then margin. Shorthand is fine when supported, but explicit longhand survives audits and diff reviews more cleanly. Build-time compilation matters for teams with design tokens. Your repository can author --text-primary in a source template; your ESP or static generator should resolve it to #334155 before the message hits SMTP. Recipients never see your token layer. They see whatever survived the last mile.
Hex discipline and the inheritance trap
Six-digit hex (#334155) remains the safest default for text color across clients. Can I email reports strong support for #rrggbb in Outlook, Gmail, Apple Mail, Yahoo, and most mobile clients. Three-digit hex works in many places but creates ambiguity in QA spreadsheets. Always expand to six digits in production templates.
rgb() and rgba() enjoy broad support today, but Outlook’s Word renderer historically choked on certain space-separated modern syntax variants. If you use functional notation, stick to comma-separated legacy form in email until your test matrix proves otherwise.
<span style="color:rgb(51,65,85);">Account ending in 4421</span>
Do not rely on oklch(), lch(), lab(), color-mix(), or currentColor in production email text today. CSS custom properties may strip or fail to resolve in Gmail and Outlook. Some teams emit variables at build time and compile to hex before send, which is a pipeline choice, not a runtime one.
A pattern that confuses web developers migrating to email: color on a wrapper <div> inside a <td> may not cascade to paragraphs in Outlook the way it does in Chrome. Color belongs on the text element, not on a distant ancestor you hope will inherit.
<td style="background-color:#ffffff;padding:24px;">
<p style="color:#334155;font-size:16px;line-height:24px;margin:0;">
Your order shipped.
</p>
</td>
The outer cell owns the white card. The paragraph owns the slate body copy. If a second paragraph needs a lighter caption tone, it gets its own inline color. Headings, spacer text, legal disclaimers, and preheader ghosts each need explicit colors if they differ from defaults. Semantic naming on the web prevents rebrand chaos; email inherits the exported hex, not the variable name. See Naming Color Tokens for the web-side discipline that keeps those exports stable.
Tinted cells and the contrast spreadsheet
Muted text on tinted backgrounds fails WCAG more often in email than teams expect, because the tint usually lives on a <td> and the text color was chosen against white in the design file. #94a3b8 on #ffffff might pass large-text thresholds. #94a3b8 on #f1f5f9 often does not. The failure shows up only after engineering moves background from the artboard’s implicit white to the template’s actual outer cell.
| Text token | Hex | Background cell | Ratio (body 16px) | Pass AA? |
|---|---|---|---|---|
| Body | #334155 |
#ffffff |
8.9:1 | Yes |
| Caption | #64748b |
#ffffff |
5.7:1 | Yes |
| Caption | #64748b |
#f1f5f9 |
4.6:1 | Borderline |
| Muted | #94a3b8 |
#f1f5f9 |
2.8:1 | No |
Run every combination you ship through WebAIM’s contrast checker or our contrast checker. Fix failures by darkening text or lightening the cell. Dark mode adds a second matrix. Apple Mail and some Gmail builds apply prefers-color-scheme: dark overrides when you provide them in a <style> block, but the base inline color still displays everywhere else.
<style>
@media (prefers-color-scheme: dark) {
.dm-text { color: #e2e8f0 !important; }
.dm-muted { color: #94a3b8 !important; }
}
</style>
<p class="dm-text" style="color:#334155;font-size:16px;">
Your renewal date is June 1.
</p>
Inline color is the fallback. The class supplies the dark override where supported. Test both the inline-only path and the override path. Recipients on clients that ignore dark mode must still read the message without squinting.
Case study: Footer gray on gray in Outlook 365
A fintech company sent monthly account statements to two point one million active users. The template had lived for four years with minimal changes: white body, slate headline, blue links, gray footer band with muted legal copy. Rebrand introduced new tokens. Body text moved from #1e293b to #334155. Footer band moved from #f8fafc to #f1f5f9. Caption text moved from #475569 to #64748b. Design signed off on contrast pairs against white in Figma. Email engineering updated the MJML partials in one afternoon.
The QA screenshot that opened the incident showed the physical mailing address, unsubscribe link, and regulatory disclaimer rendered in what looked like default black on a very light gray—or in some Outlook builds, gray on gray so close in luminance that the block read as texture instead of text. The product manager assumed the ESP had corrupted the HTML. The engineer pulled the MIME and found the hex values intact. The problem was placement and inheritance, not values.
In the footer module, color had been applied once on a wrapper <div> nested three tables deep: <div style="color:#64748b;"> wrapping four <p> tags and a link row. Gmail web and Apple Mail iOS inherited correctly. Outlook 365 on Windows did not. Paragraphs fell back to near-black system text while the cell background #f1f5f9 painted correctly on the <td>. The muted gray the team intended appeared only on one <span> that had been hand-tagged during an earlier hotfix. The rest of the footer was effectively default text on tinted cell—readable in some builds, failed contrast in others, and visually “wrong” in all of them relative to brand.
The remediation spanned four days because the footer was not an isolated module and because the statement template was localized into six languages, each with its own partial inheriting the broken wrapper pattern. Twelve other partials used the same wrapper-div pattern for “DRY” color application. Audit grep found thirty-seven instances. Each required moving color to the text-bearing element. Headlines needed separate stacks for hero, section title, and card title because font sizes differed and Outlook duplicated <p> margins if not zeroed inline. Links needed explicit color on <a> tags with text-decoration:underline because :link pseudo-classes do not exist in inline CSS the way they do on the web.
Contrast rework followed placement rework. The caption on #f1f5f9 measured 4.6:1 for sixteen-pixel body—borderline AA, failing for any reviewer using APCA mental math on small legal text. The team darkened footer copy to #475569 and restored the outer band to #f8fafc where the lighter caption still passed. They documented both pairs in a spreadsheet attached to the template repo so the next rebrand could not repeat the white-artboard assumption.
Dark mode testing surfaced a second footer issue. With @media (prefers-color-scheme: dark) overrides on classes only, Gmail Android left inline #64748b on #f1f5f9 while Apple Mail inverted both to light-on-dark correctly. The fix was inline fallback colors that passed in light mode plus !important class overrides tested per client. Preheader text using color:#ffffff on a white outer cell had been invisible in light mode by design but flashed visible when dark mode inverted the outer cell without inverting the preheader span. Preheader moved to a dedicated hidden row with display:none patterns confirmed in the test matrix, and legally required unsubscribe copy was never hidden.
The send-delay cost one business day. Legal approved the resend language. Post-fix monitoring through Litmus covered Outlook 2016, Outlook 365 Windows, Gmail web, Gmail iOS, Apple Mail macOS, and Yahoo. The ticket closure note became template for the org: every text-bearing element gets explicit six-digit hex inline; every text-background pair appears in the contrast spreadsheet; wrapper <div> color is forbidden in presentation tables. Support volume on “can’t read footer” went to zero for six months. The engineer’s summary was terse. Email text color is tedious. It is also the part recipients actually read when images fail, styles strip, and the wrapper you trusted never painted.
Client quirks and the send checklist
Outlook’s Word engine makes very light grays disappear on white. Minimum practical body gray for Outlook-heavy B2B lists is often darker than brand guidelines prefer. #475569 instead of #64748b is a common compromise. Outlook also duplicates margins on <p> tags. Zero out margin:0 inline and use <td> padding for vertical rhythm.
Gmail clips messages over 102KB HTML and may strip <style> entirely in clipped versions. Inline text color is non-negotiable for anything below the fold. Gmail’s dark theme sometimes inverts colors without asking. High-contrast inline colors reduce illegibility when inversion misfires. Apple Mail generally respects embedded CSS, which can lull you into skipping inline color on secondary spans. Do not skip. Forwarded messages still favor inline. Yahoo and AOL deserve the same conservatism as Gmail.
Preheader text often uses color:#ffffff or font-size:1px tricks to hide boilerplate after the subject preview. Gmail and Apple Mail have both changed handling of hidden text over the years. Never hide legally required unsubscribe copy. Legal footers need the same contrast discipline as marketing body copy. Regulators do not grant exemptions for eleven-pixel gray on gray.
Link color follows the same inline discipline as body copy because :link and :visited do not exist in the inline CSS world the way they do on the web. Every anchor needs explicit color and usually text-decoration:underline if affordance must survive clients that strip embedded link styles. Visited state is rarely worth separate hex in email because clients disagree on whether visited applies inside webmail chrome. Focus on default and hover-like emphasis through weight or underline rather than pseudo-classes.
Multilingual templates introduce font stacks that interact with color perception. CJK typefaces at sixteen pixels often need darker grays than Latin copy at the same size to achieve equivalent readability. If your token export assumes Inter and your Japanese partial ships Noto Sans CJK, rerun contrast review on the localized modules rather than inheriting hex from the English row without inspection.
Accessibility in email is uneven. Screen reader support for semantic headings is good when <h1> through <h3> are real tags with inline font-size and color, not styled <td> text pretending to be headings. Color alone must never encode required state. Pair hue with text (“Required field”) because not every client exposes @media (prefers-contrast) and not every recipient uses it.
ESP preprocessing is a hidden variable. Some platforms rewrite your HTML, strip comments, merge duplicate attributes, or re-inline styles with different specificity. Paste the HTML into a plain SMTP test without ESP preprocessing after major template changes to catch double-inlining bugs where the platform added color and your partial already had it, producing invalid or dropped declarations. The MIME source your deliverability team archives should match the source in your git tag for the send.
Send a real message through your ESP to personal inboxes on the same clients previews claim to represent. Disable images and confirm text color still carries brand. Forward to a different address because some clients strip <style> on forward. Document failures with screenshots attached to the ticket. When you are done, every text-bearing element has explicit inline hex, every pair is in the spreadsheet, and you have proof for Outlook, Gmail, and Apple Mail. Inline hex is not a compromise. It is the delivery mechanism. The stylesheet you wrote in the repository is a specification. The inline attribute on each tag is the shipment. Treat them as two linked artifacts, not as duplicates.