GODRICH

Marketing Email Design: 9 Templates, Outlook-Safe

Marketing email design shapes the emails you send to sell — newsletters, coupons, countdowns, reminders.

Auto-plays · click a tile to jump to its section · all nine in one zip

The order is not a trend list; it is the order in which one person receives your mail. It opens with what arrives on a schedule — a digest where three story rows rise in turn (01). Next come the three that make buying easier: a coupon code you tap to copy (02), a countdown strip whose bar drains toward its right edge (03), and the cart someone left behind, shown back to them (04). Then the three that ask about the relationship: an RSVP row of three buttons (05), the post-signup checklist (06), and a one-tap star survey (07). The last two bring people back and spread the word — missed-while-away numbers that roll up (08) and a referral code with two of three invite dots filled (09). Stage colors rotate through three ink, two yellow, two orange and two paper so no two neighbors glare. Receipts, shipping notices and security alerts — the emails that arrive after a purchase — already live in 9 Transactional Email Templates; this edition is about the mail you send to sell, which comes first.

01Newsletter digest

Three story rows rise from below, 120ms apart, each carrying a reading-time badge so the reader can pick what to open first. Tapping a row marks it read, and because the rows stay standing for most of the loop, no captured frame is an empty screen (changed area 10.75%, 13 of 23 frames move). Weekly newsletters and content subscription emails.

table role=presentationanimation-fill-mode: backwardsanimation-delay
// rows rise in turn — delayed rows need backwards or they flash complete at frame 0
.mk.is-demo .mk__row {
  animation-name: nlRise;
  animation-duration: 2s;
  animation-timing-function: $easing;
  animation-iteration-count: infinite;
}
.mk.is-demo .mk__row:nth-child(2) { animation-delay: 120ms; animation-fill-mode: backwards; }
.mk.is-demo .mk__row:nth-child(3) { animation-delay: 240ms; animation-fill-mode: backwards; }

@keyframes nlRise {
  0%   { opacity: 0; transform: translateY(14px); }
  30%  { opacity: 1; transform: translateY(0); }
  80%  { opacity: 1; transform: translateY(0); }
  88%  { opacity: 0; transform: translateY(8px); }
  100% { opacity: 0; transform: translateY(14px); }
}

02Discount coupon email

Tapping the code inside the dashed box runs navigator.clipboard.writeText, flips the label to Copied in a single frame, and reports the result through an aria-live region. In the loop the box swells once and settles, pointing at where the code lives. Seasonal sale announcements and first-purchase coupons.

navigator.clipboard.writeTextsteps(1, end)aria-live
coupon.addEventListener('click', function () {
  mk.classList.remove('is-demo');
  coupon.classList.add('is-copied');
  live.textContent = '코드를 복사했어요';
  if (navigator.clipboard && navigator.clipboard.writeText) {
    navigator.clipboard.writeText('SUMMER30').then(function () {}, function () {
      live.textContent = '복사 실패 — 코드를 직접 선택하세요';
      coupon.classList.remove('is-copied');
    });
  }
});

03Flash sale countdown strip

The seconds cell swaps three stacked values with steps(1, end) cuts — never a crossfade, so no ghost digits — while the bar beneath drains with transform-origin: right. Twenty-two of 23 captured frames move — second only to 08, where all 23 do. Flash sale blasts and deadline reminders.

transform-origin: rightgrid-template-columnssteps(1, end)
// seconds swap — three stacked layers exchanged only by steps cuts (crossfade forbidden)
.mk.is-demo .mk__d1 { animation-name: fsD1; animation-duration: 2s;
  animation-timing-function: steps(1, end); animation-iteration-count: infinite; }
.mk.is-demo .mk__d2 { animation-name: fsD2; animation-duration: 2s;
  animation-timing-function: steps(1, end); animation-iteration-count: infinite; }
.mk.is-demo .mk__d3 { animation-name: fsD3; animation-duration: 2s;
  animation-timing-function: steps(1, end); animation-iteration-count: infinite; }
.mk.is-demo .mk__fill { animation-name: fsBar; animation-duration: 2s;
  animation-timing-function: linear; animation-iteration-count: infinite; }

@keyframes fsBar {
  0%  { transform: scaleX(1); }
  78% { transform: scaleX(.16); }
  82% { opacity: 0; transform: scaleX(.16); }
  84% { opacity: 0; transform: scaleX(1); }
  90%, 100% { opacity: 1; transform: scaleX(1); }
}

04Abandoned cart reminder

Two saved item rows slide in from the left and the total cuts from ₩0 to ₩41,600 in one frame. The quantity steppers are real: every tap has the script recompute the total, re-rendered by toLocaleString, and the slide-in covers a changed area of 8.19%. Store cart reminders and back-in-stock notices.

translateXtoLocaleString-webkit-text-size-adjust
// money notation follows the page language (ko 원 · en ₩ · ja ウォン)
function money(n) {
  var lang = document.documentElement.lang || 'ko';
  var digits = n.toLocaleString('ko-KR');
  if (lang === 'ja') { return digits + 'ウォン'; }
  if (lang === 'en') { return '₩' + digits; }
  return digits + '원';
}

function recompute() {
  var items = document.querySelectorAll('.mk__item');
  var sum = 0;
  for (var i = 0; i < items.length; i++) {
    var qty = parseInt(items[i].querySelector('.mk__n').textContent, 10) || 0;
    sum += qty * parseInt(items[i].getAttribute('data-price'), 10);
  }
  total.textContent = money(sum);
}

05Event invite with RSVP

Three buttons — going, maybe, no — stand beside the date card, and in the loop the yes button cuts to its pressed state as a check icon pops. The pressed state is real: aria-pressed carries the choice, and pressing one option deselects the previous. Webinar invitations and booking confirmations.

aria-pressedcubic-beziertransform: scale
// yes-button cut — real clicks get a smooth is-on transition, the cut is for the grid loop only
.mk.is-demo .mk__yes {
  animation-name: evYes;
  animation-duration: 2s;
  animation-timing-function: steps(1, end);
  animation-iteration-count: infinite;
}
.mk.is-demo .mk__yes .mk__check {
  animation-name: evCheck;
  animation-duration: 2s;
  animation-timing-function: cubic-bezier(.2, 1.4, .4, 1);
  animation-iteration-count: infinite;
}

@keyframes evCheck {
  0%, 34%  { transform: scale(0); }
  44%      { transform: scale(1.25); }
  52%, 84% { transform: scale(1); }
  85%, 100% { transform: scale(0); }
}

06Onboarding checklist

A five-cell progress bar sits filled to three while the check on the fourth row draws itself along stroke-dashoffset. Pressing the row actually completes it: the fourth cell fills and aria-valuenow moves from 60 to 80. The bar is laid out as grid-template-columns: repeat(5, 1fr) so gauge cells stay exactly equal. Post-signup drip emails and feature series.

stroke-dashoffsetgrid-template-columnsaria-valuenow
.mk__line { stroke-dasharray: 1; stroke-dashoffset: 1; }
.mk__next.is-done .mk__line { stroke-dashoffset: 0; }

// preview loop: the check draws along its stroke from 8% to 38%
.mk.is-demo .mk__line {
  animation-name: obDraw;
  animation-duration: 2s;
  animation-timing-function: $ease-out;
  animation-iteration-count: infinite;
}

@keyframes obDraw {
  0%, 8%    { stroke-dashoffset: 1; }
  38%       { stroke-dashoffset: 0; }
  84%       { stroke-dashoffset: 0; }
  86%, 100% { stroke-dashoffset: 1; }
}

07Star rating survey email

Five stars fill from the left, 160ms apart, and stop at the fourth; a thank-you line then rises so the answer finishes inside the email. The stars are radios inside role="radiogroup", so screen readers hear a choice; tapping one fills everything to its left and the line becomes your score. Post-purchase satisfaction surveys and NPS collection.

role=radiogroupanimation-delay:focus-visible
.mk.is-demo .mk__star:nth-child(1) { animation-name: svFill; animation-duration: 2s;
  animation-timing-function: steps(1, end); animation-iteration-count: infinite; animation-delay: 120ms; }
.mk.is-demo .mk__star:nth-child(2) { animation-name: svFill; animation-duration: 2s;
  animation-timing-function: steps(1, end); animation-iteration-count: infinite; animation-delay: 280ms; }
.mk.is-demo .mk__star:nth-child(3) { animation-name: svFill; animation-duration: 2s;
  animation-timing-function: steps(1, end); animation-iteration-count: infinite; animation-delay: 440ms; }
.mk.is-demo .mk__star:nth-child(4) { animation-name: svFill; animation-duration: 2s;
  animation-timing-function: steps(1, end); animation-iteration-count: infinite; animation-delay: 600ms; }

@keyframes svFill {
  0%        { color: rgba(23,20,26,.22); }
  30%, 84%  { color: $stage-orange; }
  86%, 100% { color: rgba(23,20,26,.22); }
}

08Win-back email

Three stat cells roll from zero to their real values with requestAnimationFrame over 1.1s while the button rises below. All 23 captured frames move because the digits are still rolling; when the system prefers dark, a prefers-color-scheme block flips the card and lifts the numbers to sky blue. Dormant-user reactivation and subscription renewal notices.

requestAnimationFrametranslateYprefers-color-scheme
var counters = document.querySelectorAll('.mk__num');
var DURATION = 1100;
for (var c = 0; c < counters.length; c++) {
  counters[c].setAttribute('data-start', String(performance.now()));
}
function rollNumber(now) {
  var allDone = true;
  for (var i = 0; i < counters.length; i++) {
    var el = counters[i];
    var target = parseInt(el.getAttribute('data-target'), 10);
    var started = parseFloat(el.getAttribute('data-start'));
    var t = Math.min(1, (now - started) / DURATION);
    var eased = 1 - Math.pow(1 - t, 3);
    el.textContent = Math.round(target * eased).toLocaleString('ko-KR');
    if (t < 1) { allDone = false; }
  }
  if (!allDone) { requestAnimationFrame(rollNumber); }
}
requestAnimationFrame(rollNumber);

09Referral invite email

Three dots beneath the referral code pill fill up to the second and leave the third empty — one more invite finishes the reward. The delayed dots carry animation-fill-mode: backwards so nothing flashes complete at frame 0, and a copy button beside the code reports the result to an aria-live region. Referral campaigns and reward progress notices.

animation-fill-mode: backwardsletter-spacingaria-label
// two dots fill in turn — the pill's wide letter-spacing keeps the code legible at a glance
.mk.is-demo .d1 { animation-name: rfDot; animation-duration: 2s;
  animation-timing-function: $ease-pop; animation-iteration-count: infinite;
  animation-delay: 160ms; animation-fill-mode: backwards; }
.mk.is-demo .d2 { animation-name: rfDot; animation-duration: 2s;
  animation-timing-function: $ease-pop; animation-iteration-count: infinite;
  animation-delay: 380ms; animation-fill-mode: backwards; }

@keyframes rfDot {
  0%        { transform: scale(0); background: transparent; border-color: rgba(23,20,26,.2); }
  18%, 84%  { transform: scale(1); background: #5b3fc4; border-color: #5b3fc4; }
  88%, 100% { transform: scale(0); background: transparent; border-color: rgba(23,20,26,.2); }
}

Where it breaks — the trap

This genre breaks in mail apps, not browsers. You cannot ship these loops in the mail body as-is: per caniemail (tested 2026-04-07), only 24.39% of mail environments fully support CSS animation — 31.71% counting partial support. So all nine were completed as static markup first: with the preview loop (.is-demo) stripped away, every number, total, check and button still stands in place, and the loop you see in the grid is a web-preview layer on top. In the mail itself that static state is the body; loops live on the web version and landing pages. Support is fractured even inside the supporting half — 1&1 webmail cannot parse % keyframes and LaPoste and SFR mis-prefix @keyframes — the same page records both — so sticking to from/to frames is cheap armor. Dark mode is harsher: prefers-color-scheme support sits at 41.86%, Yahoo! Mail and AOL strip the query outright, and newer Outlook builds substitute their own data attributes (data-ogsc and friends). That is why 08 keeps the light card as the base and lifts its numbers to sky blue on the dark tiles (8.07:1) inside the dark block.

The second trap was a string the script writes back. The 04 total started life as a single toLocaleString('ko-KR') + '원' — but the phrase dictionary that translates the en and ja pages does its replacement once, and the moment a reader taps a stepper, the recomputed total comes back in Korean. Dictionary substitution covers first paint; every recalculation belongs to the script. A small money() helper that reads document.documentElement.lang and switches the notation (원·₩·ウォン) fixed all three locales at once. A spacing rule tripped in the same spot — a 2px gap between cells failed the 4px-grid check, and dropping it to 0 restored the rhythm. Mail cards are narrow enough that a couple of stray pixels make a table column visibly stutter, so the grid matters more here than on a browser page.

The third trap was the total's cut. Swapping ₩0 for ₩41,600 first went through a fade between two stacked labels, and every middle frame was a half-overlapped ghost amount. Exchanging them with steps(1, end) leaves exactly two states and no ghosts, while the two item rows sliding in beside it keep the frame count healthy at 17 of 23. Since the capture window is 2s, every loop period is a divisor of 2s and every return lands by 86–88%, before the last sampled frame. The nine finished sources behind these fixes live in the archive, and the password to open it is gf6nh9h4.

Accessibility

Colors were computed with the WCAG 2.1 relative-luminance formula, not eyeballed; only passing combinations carry text. The bottom four rows are combinations this palette refuses to use as text.

Foreground Background Measured Verdict
Ink #17141a White card #ffffff 18.24:1 pass
Ink 62% (composited #6f6d71) White card 5.12:1 pass
Deep blue #1d4fbb Coupon fill composited #f3f6fe 6.73:1 pass
Cream #fff7e6 CTA blue #2a5fd8 (01·04·08 text) 5.29:1 pass
Cream #fff7e6 Ink #17141a (countdown digits) 17.11:1 pass
Ink #17141a Orange #ff4d1f (CTA, date card) 5.50:1 pass
Grass #0f7a52 White card (progress bar) 5.35:1 pass
Cream #fff7e6 Grass #0f7a52 (check icons) 5.02:1 pass
Deep violet #5b3fc4 White card (dots) · pill fill composited (code) 7.11 · 6.44:1 pass
Ink #17141a Lilac #b9a0ff (copy button text) 8.28:1 pass
Sky #6ec8ff Tile composite #2a262a (dark-mode numbers) 8.07:1 pass
Cream 55% composited Tile composite #2a262a (dark-mode labels) 5.28:1 pass
Orange #ff4d1f White card 3.32:1 passes 3:1 as a filled star (graphic) — never as text
Cream #fff7e6 Orange #ff4d1f 3.11:1 fail — never used as text
Lilac #b9a0ff White card 2.20:1 fail — never used as text
Mint #4cd4a6 White card 1.86:1 fail — thumbnail decoration only

Under reduced motion (prefers-reduced-motion: reduce) all nine stop their loops and keep every value. 01 stands with all rows risen; 02 keeps the code readable; 03 freezes on the first seconds value with a full bar; 04 shows items and total complete; 05 rests the buttons unpressed with the check in place; 06 parks the fourth check drawn and completes for real on press; 07 returns the stars to gray and scores on tap; 08 stops the card's rise while its script-rolled digits still count up; 09 fixes the two filled dots.

Screen-reader plumbing: the card skeleton is a table marked role="presentation" so it is not read as a data table, and everything pressable is a button that answers to Enter. In 03 the remaining time reads as one role="timer" line with an aria-label while the stacked digit layers stay aria-hidden. The 04 steppers carry separate increase/decrease aria-labels; the 05 choices report through aria-pressed; the 06 bar reports aria-valuenow; the 07 stars are radios inside a radiogroup. Copy results in 02 and 09 land in an aria-live region, and stars and buttons show keyboard position with a :focus-visible ring. Support figures follow caniemail's CSS animation survey and prefers-color-scheme survey, the clipboard API follows MDN's Clipboard.writeText, and contrast follows W3C's minimum contrast explanation. Browse more marketing-mail material in the commerce category, the after-purchase side in 9 Transactional Email Templates, and how this site verifies code in the about page.

FAQ

Do these animations run inside mail apps?

The motion belongs to the web preview, not the mail body. caniemail (tested 2026-04-07) puts full CSS-animation support at 24.39% of mail environments, 31.71% with partial support, so the static state is what you send. All nine are complete screens with the loop stripped, so they ship as mail directly and come alive on landing pages.

What breaks first in Outlook?

Dark mode. prefers-color-scheme reaches 41.86% of mail environments; Yahoo! Mail and AOL delete the query, and newer Outlook builds substitute proprietary data attributes, per caniemail. Keep the light theme as the base and layer the dark one on top — 08 shows that structure.

How do these split from transactional emails?

Receipts, shipping and security alerts live in 9 Transactional Email Templates; this edition covers what you send to sell — newsletters, coupons, reminders. Real campaigns braid them: pack with 01, pull with 02, catch with 04. The dividing line is that transactional mail puts information first and marketing mail puts one action first.

Enter the archive password

The password is inside this article. You will find it as you read.