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
- 01 Newsletter digest
- 02 Discount coupon email
- 03 Flash sale countdown strip
- 04 Abandoned cart reminder
- 05 Event invite with RSVP
- 06 Onboarding checklist
- 07 Star rating survey email
- 08 Win-back email
- 09 Referral invite email
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.
// 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.
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.
// 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.
// 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.
// 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.
.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.
.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.
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.
// 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.