Tutorial: How to Code an Interactive Greeting Card Email
Around the holidays, many companies send an email to their customers thanking them for using the company and wishing them a happy new year. Birthday emails are also a popular way to stay in touch with customers and possibly offer a coupon or discount. For EOA's holiday email, I thought it would be fun to create a 3D card opening effect. Because of the techniques involved in CSS animation, I knew this would only work in a few clients. I coded the email to work only in iOS devices (and in webkit web browsers if enabled). You can check the email out right here. Read on to find out how I coded this email.
Pure CSS Flipping Effect
I started by looking around on Google for a pure CSS flipping effect I could use to create the card opening look. David Walsh's Create a CSS Flipping Animation blog was incredibly helpful, and got me started with some good code. As you can see from the demo, the effects he created flip the whole image along a center axis. I was able to build on this effect to flip from one side instead (like a hinge), and add an "inside" to the card that the reader can view once they "open" the card. The card opening effect works on hover on desktop clients and works with a tap on mobile devices. You may want to include some text in the email or the image itself to prompt readers to interact with the card, and customize this for "hover" on desktop or "tap" on mobile.HTML
I'll start with the HTML for the card holder and flip container elements.<table class="card_holder" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-spacing:0;max-width: 600px;">
<tr>
<td align="center">
<div class="card_container" style="position: relative;">
<div class="flip-container kinetic" style="text-align:center;width:100%;">
</div>
</div>
</td>
</tr>
</table>
As you can see, the card_container div is set to position: relative. This will enable us to position the elements inside of the container, which is how we apply animations to them. Now we'll add in the div that will actually flip.
<div class="card_container" style="position: relative;">
<div class="flip-container kinetic" style="text-align:center;width:100%;">
<div class="flipper">
<div class="front">
<img width="100%" src="https://www.example.com/image_front.jpg" style="max-width:600px;" />
</div>
<div class="back">
<img width="100%" src="https://www.example.com/image_back.jpg" style="max-width:600px;" />
</div>
</div>
</div>
</div>
You'll notice that inside the flipper div, we have a front div and a back div, each with an image inside. When the card opens, these two images create the front and back of the front flap of the greeting card. Now we need to add the inside of the card, the part that holds a message for the recipient.
<div class="card_container" style="position: relative;">
<div class="flip-container kinetic" style="text-align:center;width:100%;">
<div class="flipper">
<div class="front">
<img width="100%" src="https://www.example.com/image_front.jpg" style="max-width:600px;" />
</div>
<div class="back">
<img width="100%" src="https://www.example.com/image_back.jpg" style="max-width:600px;" />
</div>
</div>
<div class="inside" style="">
<img width="100%" src="https://www.example.com/image_inside.jpg" style="max-width:600px;" />
</div>
</div>
</div>
The "inside" of the card is outside of the "flipper" div so that it won't move when the card opens, but inside of the "flip-container" so that we can position it under the front of the card. That's it for HTML! We'll come back to this code when we add in fallbacks, though.
CSS
Now we'll build out the CSS that will animate our card. To make sure that this code triggers only on iOs devices, I used the following media query.@media (max-width: 780px) and (-webkit-min-device-pixel-ratio: 1) { … }
This width setting should cover all iPads and iPhones, and -webkit-min-device-pixel-ratio ensures that only webkit devices with high pixel density (iPhones and iPads) will show the code. The greeting card we created for this email is square, and we want it to display at 600px wide. So we'll set some styles for that first.
@media (max-width: 780px) and (-webkit-min-device-pixel-ratio: 1) {
.card_holder {
height:600px;
}
.card_container {
height:600px;
}
/* front pane, placed above back */
.front {
z-index: 10;
width:600px;
height:600px;
padding:0px;
/* for firefox 31 */
transform: rotateX(0deg);
}
/* back, initially hidden pane */
.back {
z-index: 5;
width:600px;
height:600px;
padding:0px;
transform: rotateX(180deg);
-webkit-transform: rotateX(180deg);
}
.inside {
z-index: -1;
width:600px;
height:600px;
padding:0px;
position: absolute;
top: 0px;
left: 0px;
/* for firefox 31 */
}
}
As you can see, we've set .back to transform: rotateX(180deg). This is what flips the back around so that it faces the inside of the card instead of facing out like a normal image would.
Because our flip_container also has the inside of the card in it we'll need to position the whole container. Setting the perspective on this is what allows us to see the card opening outwards towards the user. This would flip the front of the card over, like the demo David created. To make the card open on a seam or hinge instead, we'll change the width setting of the flip container to 0.
/* entire container, keeps perspective */
.flip-container {
-webkit-perspective: 1000px;
perspective: 1000px;
z-index: 10;
position: absolute;
top: 0px;
left: 0px;
width: 0px;
}
Then we'll add the flip effect, which triggers on hover. For iOS devices, this will trigger on tap. We're setting it to -180 degrees so that the card opens outward, towards the user. Otherwise the front of the card would seem to disappear into the inside of the card.
/* flip the pane when hovered */
.flip-container:hover .flipper {
transform: rotateY(-180deg);
-webkit-transform: rotateY(-180deg);
}
Now we'll add in some control over the speed of the card opening effect. Setting it to 2 seconds allows enough time for the user to see what's going on.
/* flip speed goes here */
.flipper, .inside {
transition: 2s;
transform-style: preserve-3d;
-webkit-transform-style: preserve-3d;
position: relative;
}
We'll want to hide the back of the front and back containers, so that the "back" container can appear as the back (instead of the actual back of the front container, which is just white), and so on.
/* hide back of pane during swap */
.front, .back {
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
position: absolute;
top: 0;
left: 0;
}
And that's our CSS! We should now have a working example that will "open" in iOS clients.
Building the Fallback
In order to send it out, we'll want to build a fallback. This uses one of Justin Khoo's excellent fallback strategies for interactive email. We'll create another content block (shown by default) for users who can't see the interactive part of this email. Because the card won't make much sense without animation, we'll just serve them a totally different image. To hide the interactive section by default, I added a few different styles (for most email clients) and wrapped the container in a !mso conditional statement (just for Outlook).<!-- START INTERACTIVE SECTION -->
<!--[if !mso]><!-->
<div class="card_container" style="position: relative;">
<div class="flip-container kinetic" style="mso-hide:all;display:none;max-height:0;overflow:hidden;text-align:center;width:100%;">
...
</div>
</div>
<!--<![endif]-->
<!-- END INTERACTIVE SECTION -->
<!-- START FALLBACK SECTION -->
<center>
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="fallback" style="margin: 0; padding: 0;">
<tr>
<td class="padding" style="margin: 0; padding: 0px;">
<img src="https://www.example.com/image_fallback.jpg" width="100%" style="max-width:600px;" />
</td>
</tr>
</table>
</center>
<!-- END FALLBACK SECTION -->
Just a bit of added CSS will make this function properly. To our media query from above, we'll add the following.
.kinetic {
display:block !important;
max-height:none !important;
height:600px !important;
width:0px !important;
overflow:visible !important;
}
.fallback {
display:none !important;
}
The kinetic styles override what we had been using to hide the content block, while the fallback style hides the fallback content. In this way, we can make sure that anything that triggers the media query that provides animation for the card also performs the show/hide function. Thus, the interactive version will only be shown in clients that support the interactivity.
Completed Code Sample
You can see the completed code sample below. You can send this to any iOS device to see the interactive behavior. If you modify the media query, you can also see this behavior in webkit browsers.<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<style type="text/css">
@media (max-width: 780px) and (-webkit-min-device-pixel-ratio: 1) {
.card_holder {
height:567px;
}
.card_container {
height:567px;
}
/* entire container, keeps perspective */
.flip-container {
-webkit-perspective: 1000px;
perspective: 1000px;
z-index: 10;
position: absolute;
top: 0px;
left: 0px;
width: 0px;
}
/* flip the pane when hovered */
.flip-container:hover .flipper {
transform: rotateY(-180deg);
-webkit-transform: rotateY(-180deg);
-ms-transform: rotateY(-180deg);
}
/* flip speed goes here */
.flipper, .inside {
transition: 2s;
transform-style: preserve-3d;
-webkit-transform-style: preserve-3d;
-ms-transform-style: preserve-3d;
position: relative;
}
/* hide back of pane during swap */
.front, .back {
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
position: absolute;
top: 0;
left: 0;
}
/* front pane, placed above back */
.front {
z-index: 10;
width:500px;
height:567px;
padding:0px;
/* for firefox 31 */
transform: rotateX(0deg);
}
/* back, initially hidden pane */
.back {
z-index: 5;
width:500px;
height:567px;
padding:0px;
transform: rotateX(180deg);
-webkit-transform: rotateX(180deg);
}
.inside {
z-index: -1;
width:500px;
height:567px;
padding:0px;
position: absolute;
top: 0px;
left: 0px;
/* for firefox 31 */
}
.kinetic {
display:block !important;
max-height:none !important;
height:567px !important;
width:0px !important;
overflow:visible !important;
}
.fallback {
display:none !important;
}
}
</style>
<!--<![endif]-->
</head>
<body style="Margin:0;padding:0;min-width:100%;background-color:#286bd6;">
<table class="card_holder" border="0" cellpadding="0" cellspacing="0" width="100%" style="border-spacing:0;max-width: 500px;margin:0 auto;">
<tr>
<td height="50"> <!-- This just adds a little padding above the card so that you can see how it opens out --></td>
</tr>
<tr>
<td align="center">
<!-- START INTERACTIVE SECTION -->
<!--[if !mso]><!-->
<div class="card_container" style="position: relative;">
<div class="flip-container kinetic" style="mso-hide:all;display:none;max-height:0;overflow:hidden;text-align:center;width:100%;">
<div class="flipper">
<div class="front">
<img width="100%" src="https://www.emailonacid.com/images/blog_images/Emailology/2016/HolidayCard_Cover.jpg" style="max-width:500px;" />
</div>
<div class="back">
<img width="100%" src="https://www.emailonacid.com/images/blog_images/Emailology/2016/HolidayCard_InsideLeft.jpg" style="max-width:500px;" />
</div>
</div>
<div class="inside" style="">
<img width="100%" src="https://www.emailonacid.com/images/blog_images/Emailology/2016/HolidayCard_InsideRight.jpg" style="max-width:500px;" />
</div>
</div>
</div>
<!--<![endif]-->
<!-- END INTERACTIVE SECTION -->
<!-- START FALLBACK SECTION -->
<center>
<table border="0" width="100%" cellpadding="0" cellspacing="0" class="fallback" style="margin: 0 auto; padding: 0;">
<tr>
<td class="padding" style="margin: 0; padding: 0px;">
<img src="https://www.emailonacid.com/images/blog_images/Emailology/2016/HolidayCard_Full.jpg" width="500" style="width:100%;max-width:500px;" />
</td>
</tr>
</table>
</center>
<!-- END FALLBACK SECTION -->
</td>
</tr>
</table>
<!-- ======= end hero article ======= -->
</body>
</html>