An easy-to-follow guide to creating an interactive Flip Card game (code samples included)
Interactive elements help turn static emails into engaging experiences that invite recipients to take action. Flip cards are a simple mechanic that reveals promotional content directly within the message without sending subscribers to a landing page. In this guide, we show you how to build a Flip Card game that works across AMP, interactive HTML, and fallback versions.
How this game works
Let’s first consider what recipients experience.
The game displays a card with a front and a back side. On hover, the card shifts slightly to indicate interactivity. When the recipient clicks or taps the card, it flips and reveals the back side with a promotional message, such as a discount code or special offer.
The front side can contain teaser text, such as “Would you like a discount?” while the back side reveals the reward (for example, “Here’s 30% off! Code: WINTER”) along with a call-to-action button.
This game can be used to do the following:
- reveal discount codes in an engaging way;
- present limited-time offers or secret deals;
- introduce product launches with hidden bonuses;
- create seasonal or holiday promo reveals;
- present educational or informational content in a compact format;
- show before-and-after comparisons for products or services;
- explain product features by revealing details step by step;
- run quick quizzes or knowledge checks for subscribers.
To ensure the card works across major email clients, we build three versions within a single email. Depending on the email client each recipient uses, they will see the version supported by their environment:
- an AMP version, which works in Gmail, Yahoo, and FairEmail;
- a kinetic version built with HTML5 and CSS3, works in Apple Mail, Thunderbird, and Samsung Mail;
- a fallback version for email clients that do not support interactivity.
Now, let’s move on to the implementation process.
AMP version
Our game creation starts with a one-column structure that you need to place in your template and make visible only in AMP HTML.

Next, insert an HTML block into this structure and paste the following base code:

<style amp-custom>
.promo-wrap-amp { transform-style: preserve-3d; perspective: 100em; max-width: 600px; padding: 10px; margin: 0 auto;}
.promo-wrap-amp .promo { margin: 0 auto; transition: transform 1s; transform-origin: center; transform-style: preserve-3d; cursor: pointer; }
.promo-wrap-amp .front, .promo-wrap-amp .back { vertical-align: middle;background: #fff; border-radius: 2em; text-align: center; padding: 20px; height: 160px; }
.promo-wrap-amp .front { background-color: #333; background-image: url(https://zlnfb.stripocdn.email/content/guids/CABINET_d5761ad20e4460f1d532af0ca384c585338f45c9de8bc8f246238f618885b798/images/25e5b487504f490c94447054cd9550a4.jpeg); background-repeat: no-repeat; background-position: center; background-size: cover; }
.promo-wrap-amp .back { background: #EDFAFD; color: #097FB3; border-color: #097FB3; } @media only screen and (max-width: 600px) { .promo-wrap-amp{ width:100%;max-width:260px; overflow:hidden;} }
</style>
<div class="promo-wrap-amp">
<div class="promo">
<div class="front">
<h2 class="es-p10b" style="text-align:center;color:#fff">
Would you like a discount?
</h2>
<p style="color:#fff">
Click to flip.
</p>
</div>
<div class="back">
<h3 style="text-align:center">
Here's 20% off! <br>Code:<b> WINTER</b>
</h3>
<table cellspacing="0" width="100%" cellpadding="0">
<tbody>
<tr>
<td align="center" class="esd-block-button es-p20t es-p10b">
<span class="es-button-border" style="border-width:0px;border-color:#333333;background:#333333;border-radius:5px"><a href="https://viewstripo.email/" target="_blank" class="es-button" style="mso-border-alt:10px solid #333333;color:#ffffff;border-radius:5px;background:#333333">Shop now</a></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

At this stage, both sides of the card, the front and the back, are placed one below the other. We need to adjust the layout so that the second block becomes the reverse side of the first.
To do this, add the following styles:
.promo-wrap-amp .promo {display: table; width: 200%; transform: rotateY(0deg)translateZ(.1em)translateX(-25%); }
.promo-wrap-amp .front, .promo-wrap-amp .back { display: table-cell; width: 50%;}
.promo-wrap-amp .front { transform: translateX(50%) translateZ(.1em); }
.promo-wrap-amp .back { transform: rotateY(180deg) translateX(50%);}

These styles position the back side behind the front side and prepare the card for a 3D flip effect.
Next, add hover and click styles. On hover, the card shifts slightly. On click, it rotates and reveals the other side:

.promo-wrap-amp .promo:hover { transform: rotateY(10deg)translateZ(.1em)translateX(-25%); }
.promo-wrap-amp .promo.reveal { transform: rotateY(-180deg)translateZ(-.1em)translateX(25%); }
At this point, the card still does not flip. For the styles to work, we need to add the class reveal to the container that wraps the card.
Find the block with class="promo" and add the following AMP attribute:

[class]="(reveal == true ? 'promo reveal' : 'promo')"
Here’s what this attribute does:
- [class] is a special AMP attribute used to change the class of an element dynamically. Here, we check the value of the reveal variable. If reveal equals true, the element receives the classes promo reveal; otherwise, it keeps the class promo.
Now, update the section with class="front" with these attributes:
[aria-hidden]="(reveal == true ? true : false)" on="tap:AMP.setState({reveal: true})" role="button" tabindex="1"
Then, update the block with class="back" with these attributes:
role="button" tabindex="2" [aria-hidden]="(reveal == true ? false : true)" aria-hidden="true" on="tap:AMP.setState({reveal: false})"

Here’s a review of the key attributes used:
- aria-hidden="true" tells assistive technologies to ignore the element;
- [aria-hidden] is an AMP binding that changes the value of aria-hidden depending on the reveal variable;
- on="tap:" is the click event handler in AMP;
- AMP.setState({ }) creates or updates variables; in our case, setting reveal to true;
- role="button" indicates that the element behaves like a button, which is required when using on="tap:" on nonbutton elements;
- tabindex="1" defines the tab order and is also required when using on="tap:" on nonbutton elements.
Once these attributes are in place, the card flips on tap and reveals the back side with the promotional content.
If you want to change the text, you can do so here:

To change the link for the button:

The AMP version is now ready. You can review the full AMP code here:
<style amp-custom>
.promo-wrap-amp { transform-style: preserve-3d; perspective: 100em; max-width: 600px; padding: 10px; margin: 0 auto;}
.promo-wrap-amp .promo { display: table; margin: 0 auto; width: 200%; transform: rotateY(0deg)translateZ(.1em)translateX(-25%); transition: transform 1s; transform-origin: center; transform-style: preserve-3d; cursor: pointer; }
.promo-wrap-amp .front, .promo-wrap-amp .back { display: table-cell; vertical-align: middle; width: 50%; background: #fff; border-radius: 2em; text-align: center; padding: 20px; height: 160px; }
.promo-wrap-amp .front { transform: translateX(50%) translateZ(.1em); background-color: #333; background-image: url(https://zlnfb.stripocdn.email/content/guids/CABINET_d5761ad20e4460f1d532af0ca384c585338f45c9de8bc8f246238f618885b798/images/25e5b487504f490c94447054cd9550a4.jpeg); background-repeat: no-repeat; background-position: center; background-size: cover; }
.promo-wrap-amp .back { transform: rotateY(180deg) translateX(50%); background: #EDFAFD; color: #097FB3; border-color: #097FB3; }
.promo-wrap-amp .promo:hover { transform: rotateY(10deg)translateZ(.1em)translateX(-25%); }
.promo-wrap-amp .promo.reveal { transform: rotateY(-180deg)translateZ(-.1em)translateX(25%); }
@media only screen and (max-width: 600px) {.promo-wrap-amp{ width:100%;max-width:260px; overflow:hidden;}}
</style>
<div class="promo-wrap-amp">
<div [class]="(reveal == true ? 'promo reveal' : 'promo')" class="promo">
<div [aria-hidden]="(reveal == true ? true : false)" on="tap:AMP.setState({reveal: true})" role="button" tabindex="1" class="front">
<h2 class="es-p10b" style="color:#fff;text-align:center">
Would you like a discount?
</h2>
<p style="color:#fff">
Click to flip.
</p>
</div>
<div role="button" tabindex="2" [aria-hidden]="(reveal == true ? false : true)" aria-hidden="true" on="tap:AMP.setState({reveal: false})" class="back">
<h3 style="text-align:center">
Here's 20% off! <br>Code:<b> WINTER</b>
</h3>
<table cellpadding="0" cellspacing="0" width="100%">
<tbody>
<tr>
<td align="center" class="esd-block-button es-p20t es-p10b">
<span class="es-button-border" style="background:#333333;border-radius:5px;border-width:0px;border-color:#333333"><a href="https://viewstripo.email/" target="_blank" class="es-button" style="border-radius:5px;background:#333333;mso-border-alt:10px solid #333333;color:#ffffff">Shop now</a></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
Kinetic version built with HTML5 and CSS3
Now, it’s time to create a kinetic version, also called the interactive HTML version, built with HTML5 and CSS3. This version works in Apple Mail, Samsung Mail, and several other email clients.
We’ll start with another empty one-column structure, placed below the AMP version. Select it and set it to be visible only in HTML.

After that, insert an HTML block into this structure and paste the following code:
<style>
.promo-wrap-html {transform-style:preserve-3d;perspective: 100em;max-width: 600px;padding: 10px; margin: 0 auto; box-sizing: border-box; overflow: hidden; }
.promo-wrap-html .promo { display: table; margin: 0 auto; width: 200%; transform: rotateY(0deg)translateZ(.1em)translateX(-25%); transition: transform 1s; transform-origin: center; transform-style: preserve-3d; cursor: pointer; }
.promo-wrap-html .front, .promo-wrap-html .back { display: table-cell; vertical-align: middle; width: 50%; background: #fff; border-radius: 2em; text-align: center; padding: 20px; height: 160px; }
.promo-wrap-html .front { transform: translateX(50%) translateZ(.1em); background-color: #333; background-image: url(https://zlnfb.stripocdn.email/content/guids/CABINET_d5761ad20e4460f1d532af0ca384c585338f45c9de8bc8f246238f618885b798/images/25e5b487504f490c94447054cd9550a4.jpeg); background-repeat: no-repeat; background-position: center; background-size: cover; cursor: pointer; }
.promo-wrap-html .back { transform: rotateY(180deg) translateX(50%); background: #EDFAFD; color: #097FB3; border-color: #097FB3; }
.promo-wrap-html .promo:hover { transform: rotateY(10deg)translateZ(.1em)translateX(-25%); }
#reveal:checked~div .promo { transform: rotateY(-180deg)translateZ(-.1em)translateX(25%); }
@media only screen and (max-width: 600px) { .promo-wrap-html { padding: 0; width:100%;max-width:260px; overflow:hidden;}}
</style>
<form>
<input id="reveal" name="reveal" type="radio" style="display:none">
<div class="promo-wrap-html">
<div class="promo">
<label for="reveal" class="front">
<h2 class="es-p10b" style="color:#fff;text-align:center!important">
Would you like a discount?
</h2>
<p style="color:#fff">
Click to flip.
</p>
</label>
<div class="back">
<h3 style="text-align:center!important;font-size:20px!important">
Here's 30% off! <br>Code:<b> WINTER</b>
</h3>
<table cellpadding="0" cellspacing="0" width="100%">
<tbody>
<tr>
<td align="center" class="esd-block-button es-p20t es-p10b">
<span class="es-button-border fallback-button-1" style="border-radius:5px;border-width:0px;border-color:#333333;background:#333333"><a href="https://viewstripo.email/" target="_blank" class="es-button" style="background:#333333;mso-border-alt:10px solid #333333;border-radius:5px;color:#ffffff">Shop now</a></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</form>
The HTML version uses a similar layout, but the flip logic is built differently.

In this version, the block with class="front" is replaced with a label tag that uses the for attribute. This label is linked to an input, and the card flip is triggered through the :checked state of that input.
Important: The input is placed at the very top of the code. Do not move it. The styles use the CSS selector ~, which depends on the exact order of elements in the code. If the structure changes, the flip effect may stop working.
The styles are almost the same as in the AMP version. The main difference is the rule that flips the card when the input is checked.
Styles
Let’s review the styles used in the kinetic version.
The base styles define the card container and prepare it for a 3D flip effect. The wrapper uses transform-style: preserve-3d and perspective to create depth, while both sides of the card are positioned so they can rotate around the Y-axis.

The front and back sides share the same dimensions. The back side is rotated using transform: rotateY(180deg) so that it stays hidden until the card flips.

Next, let’s look at the flip logic. In the kinetic version, the effect depends on the :checked state of the input placed at the top of the structure. When the input becomes checked, the container rotates.
As mentioned earlier, this selector relies on the general sibling combinator ~, which is why the input must remain before the card container in the markup.
Hover styles are also included. On hover, the card shifts slightly to indicate interactivity. This movement is defined with a subtle transform and transition rule applied to the card wrapper.
Finally, transitions are applied to make the animation smooth. Both sides of the card use transition properties so that the rotation appears natural when switching between the front and back.

Together, these styles handle the following:
- the 3D perspective;
- positioning of the front and back sides;
- hover movement;
- rotation on input state change;
- smooth animation.
With these styles in place, the kinetic flip card behaves as expected in supported email clients.
Fallback version
We have come to the last part of the guide: the fallback version. A fallback is needed for email clients that do not support interactive HTML or AMP (Outlook and others).
This version keeps the same message and layout idea but without interactivity. In this example, the fallback shows the back side of the card, so every recipient still sees the offer.

We continue working on the interactive HTML block that we created for the kinetic version. Insert the following code between the </style> tag and the <form> tag:
<!--[if !mso]><!-- -->
<input type="checkbox" checked id="fallback_ctrl" class="fallback_ctrl" style="mso-hide:all;display:none !important">
<!--<![endif]-->
<!-- FALLBACK -->
<span id="fallback" class="fallback">
<table cellpadding="0" cellspacing="0" role="presentation" width="100%" style="background-color:#EDFAFD;border-radius:40px">
<tbody>
<tr>
<td align="center" esd-text="true" class="es-p20t es-p15b esd-text">
<h2 style="text-align:center!important">
Would you like a discount?
</h2>
</td>
</tr>
<tr align="center">
<td class="esd-text es-p15b">
<h3 style="text-align:center!important">
Here's 30% off! <br>Code:<b> WINTER</b>
</h3>
</td>
</tr>
<tr>
<td align="center" class="esd-block-button">
<!--[if mso]><a href="https://viewstripo.email/" target="_blank" hidden>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" esdevVmlButton href="https://viewstripo.email/" style="height:53px; v-text-anchor:middle; width:198px" arcsize="9%" stroke="f" fillcolor="#333333">
<w:anchorlock></w:anchorlock>
<center style='color:#ffffff; font-family:Manrope, sans-serif; font-size:18px; font-weight:400; line-height:18px; mso-text-raise:1px'>Shop now</center>
</v:roundrect></a>
<![endif]-->
<!--[if !mso]><!-- -->
<span class="es-button-border fallback-button-1" style="border-radius:5px;border-width:0px;border-color:#333333;background:#333333"><a href="https://viewstripo.email/" target="_blank" class="es-button" style="background:#333333;mso-border-alt:10px solid #333333;color:#ffffff;border-radius:5px">Shop now</a></span>
<!--<![endif]-->
</td>
</tr>
<tr align="center">
<td class="es-p15b">
</td>
</tr>
</tbody>
</table>
</span>
<!-- /FALLBACK -->
<!--[if !mso]><!-- -->
<!-- INTERACTIVE ELEMENT -->
<div class="container" style="display:none;mso-hide:all">

Then, at the very end of the code, paste this snippet:

</div><!-- /INTERACTIVE ELEMENT -->
<!--<![endif]-->
Replace the placeholder text, and update the button links with your own.

And update the button links to your site:

Now add styles so that each email client sees only the version it can display. Add this code at the very end of the style tag:
/* --- */ @media screen and (-webkit-min-device-pixel-ratio: 0) { input.fallback_ctrl:checked~.container { display: block !important; } input.fallback_ctrl:checked~#fallback { display: none !important; } } [owa] .container { display: none !important; } [class~="x_container"] { display: none !important; } [id~="x_fallback"] { display: block !important; } @media screen and (max-width: 600px) { body[data-outlook-cycle] #fallback { display: block !important; } body[data-outlook-cycle] .container { display: none !important; } }
What is this?
This input controls whether the fallback block is shown or hidden through CSS. It is wrapped in conditional comments, so it stays hidden in Outlook desktop.
Also note this block:
<span id="fallback" class="fallback"></span>
It contains the full fallback layout. It should use a simple table-based structure that Outlook can render. In this example, it is a table with links to the web version.
The styles below control whether the fallback version is displayed. If you remove or comment them out, the fallback version will be visible, and you can adjust its design as needed, but remember to restore these styles before sending the email.

These styles do not follow a single strict rule per client; instead, they use a set of common display hacks:
- styles starting with [owa] are used for Outlook;
- [class~="x_container"] is used as a backup when [owa] does not apply;
- body[data-outlook-cycle] targets Outlook mobile on iOS and Android;
- mso-hide:all; is used to hide elements in Outlook.com.
The full code
Here is the full code for the kinetic HTML version plus the fallback:
<style>
.promo-wrap-html {transform-style:preserve-3d;perspective: 100em;max-width: 600px;padding: 10px; margin: 0 auto; box-sizing: border-box; overflow: hidden; } .promo-wrap-html .promo { display: table; margin: 0 auto; width: 200%; transform: rotateY(0deg)translateZ(.1em)translateX(-25%); transition: transform 1s; transform-origin: center; transform-style: preserve-3d; cursor: pointer; } .promo-wrap-html .front, .promo-wrap-html .back { display: table-cell; vertical-align: middle; width: 50%; background: #fff; border-radius: 2em; text-align: center; padding: 20px; height: 160px; } .promo-wrap-html .front { transform: translateX(50%) translateZ(.1em); background-color: #333; background-image: url(https://zlnfb.stripocdn.email/content/guids/CABINET_d5761ad20e4460f1d532af0ca384c585338f45c9de8bc8f246238f618885b798/images/25e5b487504f490c94447054cd9550a4.jpeg); background-repeat: no-repeat; background-position: center; background-size: cover; cursor: pointer; } .promo-wrap-html .back { transform: rotateY(180deg) translateX(50%); background: #EDFAFD; color: #097FB3; border-color: #097FB3; } .promo-wrap-html .promo:hover { transform: rotateY(10deg)translateZ(.1em)translateX(-25%); } #reveal:checked~div .promo { transform: rotateY(-180deg)translateZ(-.1em)translateX(25%); } @media only screen and (max-width: 600px) { .promo-wrap-html { padding: 0; width:100%;max-width:260px; overflow:hidden;}} /* --- */ @media screen and (-webkit-min-device-pixel-ratio: 0) { input.fallback_ctrl:checked~.container { display: block !important; } input.fallback_ctrl:checked~#fallback { display: none !important; } } [owa] .container { display: none !important; } [class~="x_container"] { display: none !important; } [id~="x_fallback"] { display: block !important; } @media screen and (max-width: 600px) { body[data-outlook-cycle] #fallback { display: block !important; } body[data-outlook-cycle] .container { display: none !important; } }
</style>
<!--[if !mso]><!-- -->
<input id="fallback_ctrl" type="checkbox" checked class="fallback_ctrl" style="mso-hide:all;display:none !important">
<!--<![endif]-->
<!-- FALLBACK -->
<span id="fallback" class="fallback">
<table cellpadding="0" cellspacing="0" role="presentation" width="100%" style="background-color:#EDFAFD;border-radius:40px">
<tbody>
<tr>
<td align="center" esd-text="true" class="es-p20t es-p15b esd-text">
<h2 style="text-align:center!important">
Would you like a discount?
</h2>
</td>
</tr>
<tr align="center">
<td class="esd-text es-p15b">
<h3 style="text-align:center!important">
Here's 30% off! <br>Code:<b> WINTER</b>
</h3>
</td>
</tr>
<tr>
<td align="center" class="esd-block-button">
<!--[if mso]><a href="https://viewstripo.email/" target="_blank" hidden>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" esdevVmlButton href="https://viewstripo.email/" style="height:53px; v-text-anchor:middle; width:198px" arcsize="9%" stroke="f" fillcolor="#333333">
<w:anchorlock></w:anchorlock>
<center style='color:#ffffff; font-family:Manrope, sans-serif; font-size:18px; font-weight:400; line-height:18px; mso-text-raise:1px'>Shop now</center>
</v:roundrect></a>
<![endif]-->
<!--[if !mso]><!-- -->
<span class="es-button-border fallback-button-1" style="border-radius:5px;border-width:0px;border-color:#333333;background:#333333"><a href="https://viewstripo.email/" target="_blank" class="es-button" style="border-radius:5px;background:#333333;mso-border-alt:10px solid #333333;color:#ffffff">Shop now</a></span>
<!--<![endif]-->
</td>
</tr>
<tr align="center">
<td class="es-p15b">
</td>
</tr>
</tbody>
</table>
</span>
<!-- /FALLBACK -->
<!--[if !mso]><!-- -->
<!-- INTERACTIVE ELEMENT -->
<div class="container" style="display:none;mso-hide:all">
<form>
<input type="radio" id="reveal" name="reveal" style="display:none">
<div class="promo-wrap-html">
<div class="promo">
<label for="reveal" class="front">
<h2 class="es-p10b" style="text-align:center!important;color:#fff">
Would you like a discount?
</h2>
<p style="color:#fff">
Click to flip.
</p>
</label>
<div class="back">
<h3 style="text-align:center!important;font-size:20px!important">
Here's 30% off! <br>Code:<b> WINTER</b>
</h3>
<table cellspacing="0" width="100%" cellpadding="0">
<tbody>
<tr>
<td align="center" class="esd-block-button es-p20t es-p10b">
<span class="es-button-border fallback-button-1" style="border-radius:5px;border-width:0px;border-color:#333333;background:#333333"><a href="https://viewstripo.email/" target="_blank" class="es-button" style="color:#ffffff;background:#333333;mso-border-alt:10px solid #333333;border-radius:5px">Shop now</a></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</form>
</div>
<!-- /INTERACTIVE ELEMENT -->
<!--<![endif]-->
Wrapping up
You now have a complete flip card game that works across AMP-supported clients, interactive HTML environments, and fallback-only email clients.
You can adjust the front and back content, change the animation styles, or use the card to reveal different types of offers. The structure remains the same, so you can reuse it for seasonal campaigns, product launches, or limited-time promotions.
0 comments