Click to Sign Up for a 7 Day Free Trial!

Email Development

Tutorial: Build an Interactive Carousel for Email

Email On Acid

Interactive email is becoming more popular than ever. Carousels can be a powerful tool for email, especially for mobile where vertical space is at a premium. The carousel we'll be creating below isn't just for images though, you can add any content you want to each section (including buttons and text). This tutorial is based on the work of Sam Johnston, you should check out his original article.

If you're just dying to see the code we'll be using for this tutorial, check it out on Codepen. This carousel has a few components, and we'll handle each of them separately.

Considerations

One big advantage of Sam's technique is that in email clients that don't support this kind of interactivity, the email client will instead render the content blocks as a stack. In some other carousel builds, only the first content block is rendered in fallback clients. This may be preferable for you, depending on what content you have in the carousel. I'll also note that for this carousel, the content blocks have a fixed height, and each block must have content of the same height to make it fit in the space provided.

Basic Code

First we'll need to build the basic frame of an email, so that we can start dropping our carousel code in.

<!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" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
  <title>Interactive Carousel</title>
  <!--[if !mso]><!-->
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <!--<![endif]-->
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

  <style type="text/css">
  </style>
</head>
<body style="background-color:#6c6c6c;" bgcolor="#6c6c6c">
  
</body>
</html>

Buttons

This is the easy part! The buttons are each just a set of labels and spans. The labels allow us to connect the button to the inputs, and the spans are used to style text and hide the buttons on unsupported clients.

Here's an example of how the buttons should be coded:

<label class="button slide3" for="slide-3" style="display: none; max-height: 0; visibility: hidden; font-size:1px; line-height:1px;">
<span class="hide" style="color:#ffffff;">Three</span>
</label>

<label class="button slide2" for="slide-2" style="display: none; max-height: 0; visibility: hidden; font-size:1px; line-height:1px;">
<span class="hide" style="color:#ffffff;">Two</span>
</label>

<label class="button slide1" for="slide-1" style="display: none; max-height: 0; visibility: hidden; font-size:1px; line-height:1px;">
<span class="hide" style="color:#ffffff;">One</span>
</label>

You can see that each label has the button class, which will be used to style it later. They also each have a unique "slide-x" class. The "for" attribute connects them to the correct checkbox, which is what we'll use to build the responsive behavior. Notice that these labels are hidden by default, using these styles: "display: none; max-height: 0; visibility: hidden; font-size:1px; line-height:1px;". This is so that the buttons will be invisible in unsupported clients.

After we add in the tables that contain the inputs and content, we'll move these labels to their correct position in the document flow.

Inputs/Content Holders

Now we'll code the inputs and the table that will hold our carousel. The input tables are coded so that each one contains the table next input table. This is done to enable the use of sibling selectors, which will create the interactive behavior.

First, let's build the carousel wrapper.

<table width="100%" border="0" cellpadding="0" cellspacing="0" bgcolor="#6c6c6c" style="width: 100% !important; table-layout:fixed;">
  <tr>
    <td valign="top" align="center">
      <div class="wrapper" style="min-width: 640px;">
        <table id="CarouselWrapper" width="600" border="0" cellpadding="0" cellspacing="0" bgcolor="#ffffff" class="shrink" style="width:600px; margin:0 auto;">
          <tr>
            <td>
              <table class="CarouselMain" width="600" bgcolor="#FFFFFF" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">
                <tr>
                  <td valign="top">
                    
                  </td>
                </tr>
              </table>
            </td>
          </tr>
        </table>
      </div>
    </td>
  </tr>
</table>

This will contain the main carousel table and float everything in the center of the email. The main carousel table holds all of the nested tables with inputs and will be resized via media queries for various screen sizes. Inside of the TD of the carousel table, we'll start to build our nested input tables.

<table width="100%" border="0" cellpadding="0" cellspacing="0" bgcolor="#6c6c6c" style="width: 100% !important; table-layout:fixed;">
    <tr>
      <td valign="top" align="center">
        <div class="wrapper" style="min-width: 640px;">
          <table id="CarouselWrapper" width="600" border="0" cellpadding="0" cellspacing="0" bgcolor="#ffffff" class="shrink" style="width:600px; margin:0 auto;">
            <tr>
              <td>
                <table class="CarouselMain" width="600" bgcolor="#FFFFFF" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">
                  <tr>
                    <td valign="top">
                      <table border="0" cellpadding="0" cellspacing="0">
                        <tr>
                          <td style="line-height:1px; font-size:0px;">
                            <!--[if mso]><p style="display: none;"><![endif]-->
                            <form action ="foo">
                            <input class="yahoo" type="radio" name="action" checked="checked" id="slide-1" style="display: none!important; max-height: 0; visibility: hidden;" />
                            <span style></span>
                            <!--[if mso]></p><![endif]-->
                            <table border="0" cellpadding="0" cellspacing="0">
                              <tr>
                                <td style="line-height:1px; font-size:0px;">
                                  <!-- Next Input Here -->

Each nested table will look the same, but we'll increment the id attribute on each input. Notice the MSO conditional code, which is used to hide these inputs from Outlook. This is Justin Khoo's technique, from Fresh Inbox. Without this code, it's difficult to hide inputs from Outlook and they will be rendered as (useless) radio buttons.

The empty spans are used to avoid an annoying bug in Outlook.com and Office 365. This bug causes styles on the input to be applied to the next element in the document, which would cause all of the carousel tables to become "display: none!important; max-height: 0; visibility: hidden;" and disappear. By adding these span tags, we give the bug a target to apply to that won't have a negative effect.

Here is an example with all inputs and buttons in the proper positions.

<table width="100%" border="0" cellpadding="0" cellspacing="0" bgcolor="#6c6c6c" style="width: 100% !important; table-layout:fixed;">
  <tr>
    <td valign="top" align="center">
      <div class="wrapper" style="min-width: 640px;">
        <table id="CarouselWrapper" width="600" border="0" cellpadding="0" cellspacing="0" bgcolor="#ffffff" class="shrink" style="width:600px; margin:0 auto;">
          <tr>
            <td>
              <table class="CarouselMain" width="600" bgcolor="#FFFFFF" cellpadding="0" cellspacing="0" style="border-collapse:collapse;">
                <tr>
                  <td valign="top">
                    <table border="0" cellpadding="0" cellspacing="0">
                      <tr>
                        <td style="line-height:1px; font-size:0px;">
                          <!--[if mso]><p style="display: none;"><![endif]-->
                          <form action ="foo">
                          <input class="yahoo" type="radio" name="action" checked="checked" id="slide-1" style="display: none!important; max-height: 0; visibility: hidden;" />
                          <span style></span>
                          <!--[if mso]></p><![endif]-->
                          <table border="0" cellpadding="0" cellspacing="0">
                            <tr>
                              <td style="line-height:1px; font-size:0px;">
                                <!--[if mso]><p style="display: none;"><![endif]-->
                                <input class="yahoo" type="radio" name="action" id="slide-2" style="display: none!important; max-height: 0; visibility: hidden;" />
                                <span style></span>
                                <!--[if mso]></p><![endif]-->
                                  
                                <label class="button slide2" for="slide-2" style="display: none; max-height: 0; visibility: hidden; font-size:1px; line-height:1px;">
                                <span class="hide" style="color:#ffffff;">Two</span>
                                </label>
                              </td>
                            </tr>
                          </table>
                          <label class="button slide1" for="slide-1" style="display: none; max-height: 0; visibility: hidden; font-size:1px; line-height:1px;">
                          <span class="hide" style="color:#ffffff;">One</span>
                          </label>
                        </td>
                      </tr>
                    </table>
                  </td>
                </tr>
              </table>
            </td>
          </tr>
        </table>
      </div>
    </td>
  </tr>
</table>

As you can see, we now have three inputs (slide-1, slide-2, slide-3) that are each nested into another table. Within the same TD as each input, we'll include the labels. Having the inputs and labels be siblings will be important when we add in the CSS later.

Underneath the last input (before the labels), we'll add the table that holds the content of the three slides we're creating.

<table border="0" cellpadding="0" cellspacing="0">
  <tr>
    <td>
      <table class="swoosh" border="0" cellpadding="0" cellspacing="0">
        <tr>
          <td class="slide1-content" >
            <table width="100%" border="0" cellpadding="0" cellspacing="0">
              <tr>
                <td valign="top" height="320">
                  <img src="https://www.emailonacid.com/images/emails/carousel/image3.jpg" width="320" />
                </td>
              </tr>
            </table>
          </td>
        </tr>
        <tr>
          <td height="10" style="line-height:1px;font-size:1px;display:block;"> </td>
        </tr>
        <tr>
          <td class="slide2-content" >
            <table width="100%" border="0" cellpadding="0" cellspacing="0">
              <tr>
                <td valign="top" height="320">
                  <img src="https://www.emailonacid.com/images/emails/carousel/image2.jpg" width="320" />
                </td>
              </tr>
            </table>
          </td>
        </tr>
        <tr>
          <td height="10" style="line-height:1px;font-size:1px;display:block;"> </td>
        </tr>
        <tr>
          <td class="slide3-content" >
            <table width="100%" border="0" cellpadding="0" cellspacing="0">
              <tr>
                <td valign="top" height="320">
                  <img src="https://www.emailonacid.com/images/emails/carousel/image4.jpg" width="320" />
                </td>
              </tr>
            </table>
          </td>
        </tr>
        <tr>
          <td height="10" style="line-height:1px;font-size:1px;display:block;"> </td>
        </tr>
      </table>
    </td>
  </tr>
</table>

The table contains six rows, each with one TD. The first, third and fifth TDs contain the content of our "slides" while the second, fourth and sixth serve as spacers. This is how the carousel will be rendered for clients that don't support this interactive technique.

Your HTML should match the Codepen sample now. That's all the HTML we'll need to build our carousel. It's time to make it function with CSS!

CSS

Now things start to get really complicated. CSS allows us to position the buttons, create animations, and move the "slides" of content in and out of the field of view. The CSS is the rug that really ties the whole email together. I'm going to break it down into a few parts to make it easier to understand.

We'll start with the basic styles that are not inside a media query.

<style type="text/css">
  body {
    margin: 0px;
    padding: 0px;
  }
  
  a img {
    border: none !important;
  }
  
  table {
    mso-table-lspace: 0pt;
    mso-table-rspace: 0pt;
  }
  
  table td {
    border-collapse: collapse;
  }
  
  .yahoo {
    display:none;
  }
</style>

The "yahoo" class is added to hide the inputs by default. You'll notice in the HTML we completed earlier that the inputs also have "display: none !important" inline, to hide them in Gmail. The inputs don't need to be displayed to function in the email. The rest of the styles in this section provide standardizing functions for various email clients, but don't relate to the carousel specifically.

Next we'll build the first media query. This one triggers on screens that are smaller than 480px in width.

<style type="text/css">
@media only screen and (max-width: 480px), (max-device-width: 480px) {
  
    .wrapper {
      min-width: 0px !important;
    }
    
    .shrink {
      width: 320px !important;
      min-width:0px !important;
    }
    
    .hide {
      display: none !important;
      visibility: none !important;
      overflow: hidden !important;
    }
  
  }
</style>

The .wrapper style removes the min-width that was set on the wrapper table. The next style, .shrink, causes content tables to slim down to 320px so they'll look good on smaller screens and overwrites the min-width already set inline on those containers (so that it doesn't conflict with the new, smaller width). The last style, .hide, is used to hide the span in each label, so that the words one, two and three can be replaced with numerals which take up less space on mobile screens.

Now let's add another layer to our CSS styles.

<style type="text/css">
  @media only screen and (-webkit-min-device-pixel-ratio: 1), (min--moz-device-pixel-ratio: 1), (max-width: 480px), (max-device-width: 480px) {
  
    #CarouselWrapper {
      width: 415px !important;
      height: 895px !important;
      position: relative;
      display: block;
      overflow: hidden;
    }
    
    .CarouselMain {
      width: 600px !important;
      height: 1400px !important;
    }

    .button {
      visibility: visible !important;
      max-height: 46px !important;
      width: 86px !important;
      padding:7px 0px 6px 0px;
      display: block !important;
      position: absolute !important;
      text-align: center;
      z-index: 7;
      background-color: #262626;
      box-sizing: content-box;
      color: #ffffff !important;
      font-family: Arial, Helvetica, sans-serif;
      font-size: 13px !important;
      line-height:16px !important;
      font-weight:bold;
      cursor:pointer;

      -webkit-transition: 0.1s ease-in;
      -moz-transition: 0.1s ease-in;
      -ms-transition: 0.1s ease-in;
      -o-transition: 0.1s ease-in;
    }
    
    .swoosh {
      position: relative;
      transition: all 0.3s ease-out;
      -webkit-transition: all 0.3s ease-out;
      -moz-transition: all 0.3s ease-out;
      -ms-transition: all 0.3s ease-out;
      -o-transition: all 0.3s ease-out;
    }
    
    .slide1-content,
    .slide2-content,
    .slide3-content {
      display: block;
      position: absolute;
      top: 0px;
      left: 0px;
      width: 600px;
      height: 520px;
      background-color: #FFFFFF; 
    }
  }
</style>

All of these styles are wrapped in a media query that only triggers on any of the following: webkit rendering engines (-webkit-min-device-pixel-ratio), mozilla rendering engines (min--moz-device-pixel-ratio), and mobile devices ((max-width: 480px), (max-device-width: 480px)). This is to ensure that partially functional code isn't shown on other email clients.

First we'll set some styles for the CarouselWrapper container so that we can move the slides within it and define its height and width. We'll also define height and width for CarouselMain.

The button style block makes the buttons visible and sets their height, width and other relevant display styles. The "z-index" style is used to make sure the buttons will be visible over the content during animation. The cursor style is important as an indicator that the user can interact with the button on desktop clients. The various prefixed transition styles control how quickly the buttons color in on hover and after changing slides.

The "swoosh" class is applied to the table that holds all of the "slide" TDs. Position: relative enables us to move the TDs around, and the transition styles with "ease-out" create the movement as the user selects a new button.

Finally, the styles attached to slide1-content, slide2-content and slide3-content set the "slides" to be absolute positioned and display block. This allows us to move them as the user selects checkboxes. It also sets their initial height, width and position.

Our next media query is set to activate only for webkit and Mozilla users, again to prevent partially functional code from showing.

<style type="text/css">
  @media only screen and (-webkit-min-device-pixel-ratio: 1), (min--moz-device-pixel-ratio: 1) {
  
    .slide1 { top: 10px; left: 321px;}
    .slide1-content {left: 0px;}
    .slide1:hover { background-color: #37B330;}
    #slide-1:checked ~ .slide1 {background-color: #37B330 !important;}
    #slide-1:checked + span + table .swoosh { left: 0px !important; }
    
    .slide2 { top: 66px; left: 321px; }
    .slide2-content { left: 600px; }
    .slide2:hover { background-color: #37B330;}
    #slide-2:checked ~ .slide2 {background-color: #37B330 !important;}
    #slide-2:checked + span + table .swoosh { left: -600px !important; }
    
    .slide3 { top: 122px; left: 321px; }
    .slide3-content { left: 1200px; }
    .slide3:hover { background-color: #37B330;}
    #slide-3:checked ~ .slide3 {background-color: #37B330 !important;}
    #slide-3:checked + span + table .swoosh { left: -1200px !important; }

  }
</style>

The first style in each set is used to position the buttons. The second style positions the "slide" container. The third slide is used to provide the background color change for the button on hover. The "#slide-1:checked ~ .slide1" style colors in the currently selected button. Finally, the "#slide-3:checked + span + table .swoosh { left: -1200px !important; }" style moves the correct slide into view when the corresponding button is checked. For more on how the + and ~ selectors work, check out this page.

We're almost done! Let's take a look at the last set of styles we'll need for this functionality.

<style type="text/css">
  @media only screen and (max-width: 480px),  (max-device-width: 480px) {
  
    #CarouselWrapper, .CarouselMain {
      width: 320px !important;
      height: 895px !important;
    }
    
    .slide1-content,
    .slide2-content,
    .slide3-content {
      top: 0px;
      width: 320px !important;
      height: 1350px !important;
      background-repeat: no-repeat;
    }
    
    .button {
      max-height: 32px !important;
      width: 32px !important;
      border: none;
      padding: 8px 0px 8px 0px;
    }
    
    .slide1 { top: 0px; left: 20px;}
    .slide1-content {left: 0px; width: 100%; }
    #slide-1:checked + span + table .swoosh { left: 0px !important;}
    .slide1:before { content: "1" }
    
    .slide2 { top: 0px; left: 61px; }
    .slide2-content { left: 320px; width: 100%; }
    #slide-2:checked + span + table .swoosh { left: -320px !important; }
    .slide2:before { content: "2"; }
    
    .slide3 { top: 0px; left: 103px; }
    .slide3-content { left: 640px; width: 100%; }
    #slide-3:checked + span + table .swoosh { left: -640px !important; }
    .slide3:before { content: "3"; }
  
  }
</style>

These styles control how the carousel should appear for mobile users. The first three style blocks resize the containers and buttons to make them better fit mobile screens. 320px width was selected to fit iPhone5 screens, but you can select whatever width works best for your content.

The last three style blocks are used to reset the carousel positioning values for mobile screens. Let's look at the four selectors and their styles in detail. ".slide1 { top: 0px; left: 20px;}" is used to move the buttons from the right side of the container to the top, where they'll be more useful for mobile readers. ".slide1-content {left: 0px; width: 100%; }" is used to reposition the slide and make it 100% width so that it fills the screen. "#slide-1:checked + span + table .swoosh { left: 0px !important;}" resets the "desktop" values for positioning and also creates slide repositioning effect. The last style, ".slide1:before { content: "1" }" changes the button from saying "One" (which was hidden using the "hide" class) and replaces it with the numeral 1.

Putting it all together

Check out the completed sample on Codepen.

Where is the carousel supported?

The carousel should work in all clients that supports webkit and moz CSS.

  • Apple Mail 7
  • Apple Mail 8
  • Outlook 2011 for Mac
  • Outlook 2016 for Mac
  • Mozilla Thunderbird
  • iPad and iPad mini iOS
  • iPhone iOS
  • Android
  • Windows Mobile
  • AOL (Chrome, Firefox & Safari)

In other email clients, the "fallback" of stacked content blocks should be shown.

  • Lotus Notes and IBM Notes
  • Outlook 2000
  • Outlook 2002
  • Outlook 2003
  • Outlook 2007
  • Outlook 2010
  • Outlook 2013
  • Outlook 2016
  • AOL (Internet Explorer)
  • Gmail App (iPhone, Windows Mobile & Android)
  • Gmail
  • Google Apps
  • Office 365
  • Outlook.com
  • Yahoo! Mail

About the Author

Geoff Phillips

Geoff Phillips

Half writer, half email builder/fixer and half customer support, Geoff is living his dream in a role that combines his many diverse interests. Code problem or tricky client got you down? Geoff's your man.

Join the Discussion

Hi Geoff!, excellent tutorial, it help me a lot with a carousel i'm working on.
Diego
Has anyone found a way to make this work on MailChimp? It seems like MailChimp strips out the form element so you can't use the checkboxes or radio elements.
Todd Packer
Hi Todd,

Unfortunately you're right about Mailchimp, it will strip all form elements. This technique will not work in Mailchimp.
Alex Ilhan
I'm having trouble with AOL. The carousel is rendering, but not working. The FreshInbox chart says AOL Mail doesn't support CSS animation, so is that why? http://freshinbox.com/resources/css.php Is there a way to hide the carousel from AOL?
javelin
Many Thanks!! awesome
I have learned a lot
After much trial and error, i finally managed to adapt your example in order to auto generate table reports from excel (using ADO STREAM to write html code with the excel data).
robertocm

Leave a Comment