Email Template Design skill for Codex
HTML email that survives Outlook and dark mode. Codex designs emails as their own platform: single-column 600px layouts in MJML with shared attributes, live-text messages, bulletproof buttons, dark mode meta and styles, alt text and a reviewed plain-text part. It checks the compiled size against Gmail clipping, previews locally through a Mailpit SMTP catcher, and never sends to real recipients without your confirmation.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- email-template-design Installs to ~/.agents/skills/email-template-design/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install email-template-design The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project email-template-design Installs to .agents/skills/email-template-design/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/email-template-design.zipunzip -o email-template-design.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) email-template-design PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $email-template-design, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs and builds HTML email templates that render in Outlook, Gmail, Apple Mail and Outlook.com - MJML as the default source format, single-column 600px layouts, bulletproof buttons, dark mode handling, preheader text, accessible tables and alt text, plain-text alternatives, Gmail clipping limits, and local preview with Mailpit before any real send. Use when creating or fixing a transactional or marketing email template, converting a design to email HTML, or debugging an email that breaks in Outlook or dark mode.
Use it when
- Creating a transactional or marketing email template
- An email breaks in classic Outlook, Gmail or dark mode
- A web design must be converted into email-safe HTML
- Emails are getting clipped by Gmail or look empty with images off
Use something else when
- Planning lifecycle campaigns, segmentation or deliverability: use email-marketing
- Writing the words of a one-off business email: use professional-email
How to check the result
MJML compiles in strict mode, the HTML is under 80 KB, and the email reads correctly at 320px, in dark mode, with images blocked and in classic Outlook or a rendering service.
The mistake it prevents
Using web CSS like flexbox, max-width or div margins, which classic Outlook for Windows ignores and turns into a broken full-width layout.
A finished email reads correctly with images off, in dark mode, at 320px wide and in classic Outlook for Windows; the important message and action are visible above the fold; the HTML stays under Gmail’s clipping limit; and a plain-text part says the same thing. Email HTML is its own platform: web CSS habits break here.
Source format
| Situation | Use |
|---|---|
| New templates, no existing system | MJML: compiles to table-based HTML with Outlook conditionals included |
| React codebase already sending with React Email | Keep React Email components; same design rules apply |
| Team uses Tailwind and wants a build pipeline | Maizzle |
| One-off fix to an existing hand-coded template | Edit the HTML; read references/client-quirks.md first |
Read AGENTS.md and look for existing templates and the sending path before choosing
(rg -l "mjml|@react-email|maizzle|sendgrid|postmark|resend|nodemailer|ses" --glob '!**/node_modules/**').
Templates often contain placeholders for an ESP or a templating engine (Handlebars,
Liquid); keep their syntax intact.
Workflow
- Content first: subject, preheader, one primary message, one primary CTA.
- Layout: single column, 600px, sections stacked; two columns only for small grids.
- Build in MJML with shared
mj-attributes; compile and validate. - Add dark mode, plain text, alt text,
lang, and preheader. - Preview locally (browser + Mailpit), check size, then test in real clients.
- Send to any real inbox or list only after the user confirms recipients.
Design rules
- Width 600px; body text 16px with 24px line height; headings 22-28px.
- System or safe font stacks; web fonts only as enhancement
(
Inter, Arial, Helvetica, sans-serif). Outlook and Gmail ignore most web fonts. - One primary CTA as a button at least 44px tall, repeated at the bottom for long emails.
- Key message in live text, not inside images. Image-only emails show nothing when images are blocked and are flagged by spam filters.
- Contrast at least 4.5:1; links underlined in body copy.
- Mobile: columns stack below 480px (MJML default), tap targets at least 44px, padding 16-24px.
MJML skeleton
<mjml lang="en">
<mj-head>
<mj-title>Your invoice from Acme</mj-title>
<mj-preview>Invoice 1042 for $49.00 is ready - due 12 Nov</mj-preview>
<mj-attributes>
<mj-all font-family="Inter, Arial, Helvetica, sans-serif" />
<mj-text font-size="16px" line-height="24px" color="#1f2937" padding="0 0 16px" />
<mj-button background-color="#1f4fd8" color="#ffffff" font-size="16px" font-weight="600"
border-radius="6px" inner-padding="14px 24px" />
<mj-section padding="0" />
</mj-attributes>
<mj-style>
@media (prefers-color-scheme: dark) {
.card { background: #111827 !important; }
.card div { color: #e5e7eb !important; }
}
</mj-style>
<mj-raw>
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
</mj-raw>
</mj-head>
<mj-body width="600px" background-color="#f3f4f6">
<mj-section padding="24px 0">
<mj-column>
<mj-image src="https://cdn.example.com/email/[email protected]" width="120px" alt="Acme" align="left" padding="0 24px" />
</mj-column>
</mj-section>
<mj-section css-class="card" background-color="#ffffff" padding="32px 24px" border-radius="8px">
<mj-column>
<mj-text font-size="24px" line-height="32px" font-weight="700">Invoice 1042 is ready</mj-text>
<mj-text>Hi {{first_name}}, your invoice for <strong>$49.00</strong> is due on 12 November.</mj-text>
<mj-button href="https://app.example.com/invoices/1042">View invoice</mj-button>
<mj-text font-size="14px" color="#4b5563">Questions? Reply to this email.</mj-text>
</mj-column>
</mj-section>
<mj-section padding="24px">
<mj-column>
<mj-text font-size="12px" line-height="18px" color="#6b7280" align="center">
Acme Inc, 1 Harbor St, Springfield. <a href="{{unsubscribe_url}}" style="color:#6b7280">Unsubscribe</a>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
npx mjml src/invoice.mjml -o dist/invoice.html --config.validationLevel=strict # network on first npx run
npx mjml -w src/invoice.mjml -o dist/invoice.html # watch while editing
Add mjml as a dev dependency with the project’s package manager rather than relying on
npx in CI.
Dark mode
Clients behave in three ways: no change (some webmail), full or partial color inversion
(Gmail app, Outlook), or honouring prefers-color-scheme (Apple Mail, some Outlook).
- Declare
color-schememeta tags (as above) and provide dark styles in a media query. - Logos: use transparent PNGs with a light stroke or a version that works on both; or place the logo on a fixed light “pill” background.
- Avoid pure
#ffffff/#000000pairs; inversion engines treat near-white and near-black more gently. - Outlook.com dark mode adds
data-ogscattributes; target them only when a specific element breaks.
Images
- Host on HTTPS with absolute URLs. Export at 2x and set the display
widthattribute. - Every
<img>getsalt; decorative images getalt="". - Keep total image weight under about 1 MB; GIFs: first frame must carry the message.
- Background images need VML for Outlook; MJML’s
background-urlonmj-sectiongenerates it. Always set a fallbackbackground-color.
Plain text and headers
Generate a plain-text part with the same content and the full URLs. Most ESPs
auto-generate one; review it, because generated text often includes layout junk.
Transactional sending services also expect a List-Unsubscribe header for bulk mail,
and marketing mail legally needs an unsubscribe link and a postal address in many
jurisdictions; flag this for the user rather than assuming compliance.
Size and clipping
Gmail clips messages over about 102 KB of HTML. Check:
wc -c dist/invoice.html # aim for under 80 KB to leave room for ESP tracking rewrites
Reduce by removing comments, using mj-attributes instead of repeated inline attributes,
and minifying (--config.minify=true in MJML 4, or a separate HTML minifier).
Testing
- Open
dist/*.htmlin a browser at 600px and 320px; toggle dark mode in devtools. - Disable images (block the image host in devtools) and confirm the message survives.
- Send through a local SMTP catcher such as Mailpit (
mailpitbinary or Docker image; UI on port 8025, SMTP on 1025) so nothing leaves the machine. Point the app’s dev SMTP settings at it. - Real-client rendering: Litmus or Email on Acid (paid, needs network) or test accounts in Gmail, Outlook.com and classic Outlook for Windows.
Sending to real addresses, seed lists or customers is outward-facing: show the user the recipient list and get explicit confirmation before any send.
Gotchas
- Flexbox, grid,
position, CSS variables,max-width(Outlook), andmarginon block elements (Outlook) do not work reliably. Tables and padding on cells do. - Gmail drops the entire
<style>block if it hits a syntax error; keep critical styles inline (MJML inlinesmj-attributesoutput). - Preheader text: if the content is shorter than the preview pane, clients pull in the
next text (“View in browser”). MJML’s
mj-previewpads it; keep it 40-110 characters. - Template variables inside attributes (
href="{{url}}") must survive compilation; check the compiled HTML, especially withmj-rawand minification. - Links to
localhostor staging hosts in production templates. Grep compiled output before shipping:rg -n "localhost|staging|example\.com" dist/.
Verification checklist
-
mjml --config.validationLevel=strictcompiles with no errors. - HTML size under 80 KB; images absolute HTTPS with alt text.
- Renders at 320px and 600px; dark mode readable; images-off readable.
- Classic Outlook check done (real client or rendering service) or listed as untested.
- Plain-text part reviewed; unsubscribe and address present for marketing mail.
- No test or localhost links in compiled output.
For per-client fixes (Outlook VML, DPI scaling, Gmail style stripping), read
references/client-quirks.md when a specific client
misrenders. Lifecycle strategy and deliverability belong to $email-marketing.
What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Email Template Design"
short_description: "HTML email that survives Outlook and dark mode"
default_prompt: "$email-template-design Build a responsive invoice email in MJML that works in Outlook" references/client-quirks.md
# Email client quirks and fixes
Read this when a template renders wrong in a specific client, or before hand-editing
compiled MJML output. Support changes; when in doubt, check caniemail.com (needs network)
and confirm with a real render.
## Classic Outlook for Windows (Word rendering engine)
Applies to Outlook 2016, 2019, 2021 and the Microsoft 365 desktop app in classic mode.
The "new Outlook for Windows" renders like Outlook.com instead.
| Problem | Fix |
|---|---|
| Ignores `max-width`, so fluid layouts go full width | Wrap content in a ghost table: `<!--[if mso]><table width="600"><tr><td><![endif]-->` ... `<!--[if mso]></td></tr></table><![endif]-->` (MJML does this for you) |
| Ignores `margin` and `padding` on `div`, `p` | Put padding on `<td>`; space blocks with table cells or spacer rows |
| Ignores CSS background images | VML `<v:rect>` with `<v:fill type="frame">` inside `<!--[if gte mso 9]>`; MJML's `background-url` on `mj-section` generates this |
| No `border-radius` | Accept square corners, or VML `<v:roundrect>` for bulletproof buttons |
| Line-height inflates or collapses | `mso-line-height-rule: exactly;` alongside `line-height` in px |
| Images scale wrong at 120 DPI displays | Set `width` attribute (not only CSS) on every `<img>`, and include the `o:OfficeDocumentSettings` block with `<o:PixelsPerInch>96</o:PixelsPerInch>` in the head (MJML includes it) |
| Gaps under images | `display: block` on images; `border: 0` |
| Falls back to Times New Roman when the first font in the stack is a web font | Add `<!--[if mso]><style>body, table, td, p, a, h1, h2, h3 { font-family: Arial, sans-serif !important; }</style><![endif]-->` in the head |
| Animated GIF shows only the first frame (some versions) | Make the first frame carry the message |
| `display: none` content still shows | Add `mso-hide: all` |
## Gmail (web, iOS, Android)
| Problem | Fix |
|---|---|
| Message clipped with "[Message clipped] View entire message" | Keep the HTML under about 100 KB; minify, remove comments, avoid repeated inline styles |
| Whole `<style>` block dropped | One CSS syntax error can discard the block; validate CSS. Keep critical styles inline |
| Styles stripped for non-Gmail accounts in the Gmail app (IMAP/POP) | Inline all essential styles; treat `<style>` as progressive enhancement |
| Attribute selectors and some pseudo-classes unsupported | Use class selectors only |
| Blue auto-links on dates, addresses, phone numbers | Wrap in a span with explicit color, or accept |
| Dark mode partially inverts colors (app) | Avoid pure white on pure black logos; give logos a padded light background or transparent PNG with outline |
## Apple Mail (macOS, iOS)
- Best CSS support: media queries, web fonts, `prefers-color-scheme`.
- Auto-detects and links dates, addresses: `<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">` plus explicit link colors.
- Dark mode: honours `@media (prefers-color-scheme: dark)` when the head declares
`<meta name="color-scheme" content="light dark">` and `<meta name="supported-color-schemes" content="light dark">`.
- iOS scales up small text; `-webkit-text-size-adjust: 100%` prevents layout breaks.
## Outlook.com and new Outlook for Windows
- Dark mode rewrites colors and adds `data-ogsc` / `data-ogsb` attributes; target them
to adjust: `[data-ogsc] .text { color: #e5e5e5 !important; }`.
- Supports `<style>` and media queries reasonably; strips some properties (positioning).
## Yahoo / AOL
- Media queries are supported, but class names are rewritten; use simple class
selectors and keep essential styles inline.
## Universal rules
- Width 600-640px for the main column.
- Tables for structure (`role="presentation"` on layout tables), inline styles for
anything critical, `<style>` for responsive and dark-mode enhancements.
- Absolute URLs (https) for every image and link.
- No JavaScript, forms, video embeds, iframes or external CSS. AMP for Email needs sender
registration and is rarely worth it.
- Alt text on all images; images off by default in some corporate clients.
- Total images under about 1 MB; each image optimised and sized for 2x display.
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.