Styling Effective Carousels

This article was first published on (02-01-2015).

This article is not about designing effective carousels but about styling them effectively. In other words, this is not about UI design but CSS constructs — how carousel items flow, their positioning and dimensions.

TL;DR: use neither float nor position:absolute

Relying on JavaScript for interaction, not for styling

An "effective" carousel is a carousel that does not rely on JavaScript to:

The challenges

There are many methods of displaying items of a carousel side-by-side, however some of these methods are better than others.

Using float

The carousel on http://www.disneystore.com/ shows the two main limitations of this styling:

Disney's Carousel

To better understand the issue, consider this example below:

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
ul {
    border: 5px solid deeppink;
    overflow: hidden; /* to contain the floats */
}

li {
    border: 1px solid #fff;
    background: #3CB371;
    width: 100px;
    height: 100px;
    float: left;
}

The list abobe contains five items — all styled as floats. The list itself is styled with overflow:hidden (to create a block-formatting context) so the list encloses the floats instead of collapsing onto itself.

The problem here is that the items wrap if their container is not wide enough to contain all of them side-by-side, as shown below:

ul {
    overflow: hidden; /* to contain the floats */
    width: 450px;
}

li {
    float: left;
}

This is why this solution requires setting an explicit width on the container which prevents said container from being truly responsive (without the need for JavaScript).

Using position:absolute

In carousels like the one on http://on.aol.com/, all items are removed from the flow, hence each one of them relies on a different offset value to show next to its previous sibling.

AOL's Carousel

Consider this new example:

ul {
    height: 100px; /* prevent the container from collapsing */
}

li {
    position: absolute;
}

Because all boxes are absolutely positioned, they are removed from the flow and share the same x/y coordinates; the last box showing on top of the stack.

As a result, authors need to do 3 things:

The same boxes styled as described above:

ul {
    height: 100px; /* prevent the container from collapsing */
    position: relative; /* make this container the containing block of the nested AP boxes */
}

li {
    position: absolute;
}

li:nth-child(2) {
    left: 100px;  /* width + left/right border of 1st box */
}

li:nth-child(3) {
    left: 200px; /* width + left/right border of previous boxes */
}

li:nth-child(4) {
    left: 300px; /* width + left/right border of previous boxes */
}

li:nth-child(5) {
    left: 400px; /* width + left/right border of previous boxes */
}

This is how the carousel on aol.com is styled, with all offsets expressed in pixels. But the interesting point is that the width of the container is set the same way as in our float example. The container is styled to be as wide as the combined width of all its children — even though using position:absolute allows authors to rely on a much simpler approach, one that leverages the characteristics of containing blocks.

Unlike with float constructs, the width of the container does not play a role in the positioning of the nested boxes. This means it is possible to use percentages to display the items full width (100%) or as a fraction of their container (their containing block); for example, styling each box with 50% would display 2 of them side-by-size — as shown below:

ul {
    height: 100px; /* prevent the container from collapsing */
    position: relative; /* make this container the containing block of the nested AP boxes */
    width: 30%;
}

li {
    position: absolute;
    width: 50%;
}

li:nth-child(2) {
    left: 50%; /* width + left/right border of 1st box */
}

li:nth-child(3) {
    left: 100%; /* width + left/right border of previous boxes */
}

li:nth-child(4) {
    left: 150%; /* width + left/right border of previous boxes */
}

li:nth-child(5) {
    left: 200%; /* width + left/right border of previous boxes */
}

The width of the container above is set to 30%, all other values (left offsets and width of boxes) are also set using percentages.

ul {
    height: 100px; /* prevent the container from collapsing */
    position: relative; /* make this container the containing block of the nested AP boxes */
    width: 50%;
}

li {
    position: absolute;
    width: 25%;
}

li:nth-child(2) {
    left: 25%; /* width + left/right border of 1st box */
}

li:nth-child(3) {
    left: 50%; /* width + left/right border of previous boxes */
}

li:nth-child(4) {
    left: 75%; /* width + left/right border of previous boxes */
}

li:nth-child(5) {
    left: 100%; /* width + left/right border of previous boxes */
}

This time, the width of the container is 50% and the width of each box is 25% - which displays four boxes side-by-side inside the container.

This solution is definitely superior to using float, but the drawback here is that removing all the items from the flow requires styling the container itself with a height — to prevent the following elements in source to show behind all the boxes.

The solution

To be robust, the solution needs to make sure the containing block corresponds to the visible area of the carousel and that nothing is removed from the flow — it is the content that dictates the height of the carousel.

Using inline-block

Starting with the basic:

<ul>
    <li>1</li><!--
    --><li>2</li><!--
    --><li>3</li><!--
    --><li>4</li><!--
    --><li>5</li>
</ul>
li {
    display: inline-block;
}

With this simple styling the layout works the same as in a float construct, the nested boxes wrap if there is no room for them:

ul {
    width: 450px;
}

li {
    display: inline-block;
}

The magic bullet

We can prevent the boxes from wrapping using white-space:nowrap:

ul {
    width: 450px;
    white-space: nowrap;
}

li {
    display: inline-block;
}

Now we have a solution that does not require setting an explicit height onto the container, nor styling the nested boxes with an offset or setting their width with hard values. And as a bonus, this solution is RTL-friendly:

<ul class="example example-10" dir="rtl">
    <li>1</li><!--
    --><li>2</li><!--
    --><li>3</li><!--
    --><li>4</li><!--
    --><li>5</li>
</ul>

The carousel implementation

Offset via margin

Applying a left margin on the first element is enough to move all the boxes at once (left or right):

ul {
    width: 100px;
    white-space: nowrap;
}

li {
    display: inline-block;
    width: 100%;
}

li:first-child {
    margin-left: -100%; /* a class would be needed for oldIE */
}

Styling the container with overflow:hidden will hide the items that are outside that container:

ul {
    width: 100px;
    white-space: nowrap;
    overflow: hidden;
}

li {
    display: inline-block;
    width: 100%;
}

li:first-child {
    margin-left: -100%;
}

One thing to remember is to reset the nowrap declaration as this style is inherited.

Offset via translate, position, etc.

Moving the container rather than its first child necessitates an extra wrapper (note that all styles are transferred from the list to that wrapper):

<div>
    <ul>
        <li>1</li><!--
        --><li>2</li><!--
        --><li>3</li><!--
        --><li>4</li><!--
        --><li>5</li>
    </ul>
</div>
div {
    white-space: nowrap;
    width: 50%;
    overflow: hidden;
    border: 5px solid deeppink;
}

ul {
    border: none;
    *position: relative; /* fallback for oldIE */
    *left: -100%;        /* fallback for oldIE */
    transform: translateX(-100%);
}

/* fallback for IE8 */
@media screen\0 {
    .example-13 {
        position: relative;
        left: -100%;
    }
}

li {
    white-space: normal; /* reset */
    display: inline-block;
    width: 50%;
}
  • 1
  • 2
  • 3
  • 4
  • 5

This solution relies on such a simple construct that we can easily do something like this:

div {
    padding-right: 12%; /* create a gap to the right of the list to reveal part of the following box */
}

img {
    width: 100%; /* same width as its container */
    vertical-align: bottom;
}

Note that it is the content (the images) that dictates the height of the carousel and that all the boxes are responsive and properly positioned — without the need for JavaScript.

Pure CSS Carousel with fade-in/scale effect

A logic that does not require math skills!

<div class="carousel">
    <input role="presentation" name="carousel" type="radio" value="1" checked />
    <input role="presentation" name="carousel" type="radio" value="2" />
    <input role="presentation" name="carousel" type="radio" value="3" />
    <input role="presentation" name="carousel" type="radio" value="4" />
    <input role="presentation" name="carousel" type="radio" value="5" />
    <ul class="carousel-list">
        <li><img src="..." alt="Mask #1"></li><!--
        --><li><img src="..." alt="Mask #2"></li><!--
        --><li><img src="..." alt="Mask #3"></li><!--
        --><li><img src="..." alt="Mask #4"></li><!--
        --><li><img src="..." alt="Mask #4"></li>
    </ul>
</div>
.carousel {
    width: 200px;
    padding: 5px;
    overflow: hidden;
    border: 1px solid #ccc;
    border-radius: 3px;
    text-align: center;  /* to center the radio buttons */
}

.carousel-list {
    white-space: nowrap;
    padding: 0;
    margin: 0;
    transition: transform .3s;
}

.carousel-list li {
    white-space: normal; /* reset */
    display: inline-block;
    width: 100%;
}

.carousel-list img {
    width: 100%; /* fit the container */
    vertical-align: bottom; /* remove white-space below image */
}

/**
 * it is the radio buttons that move the list
 */
input:nth-child(1):checked ~ ul {
    transform: translateX(0);
}

input:nth-child(2):checked ~ ul {
    transform: translateX(-100%);
}

input:nth-child(3):checked ~ ul {
    transform: translateX(-200%);
}

input:nth-child(4):checked ~ ul {
    transform: translateX(-300%);
}

input:nth-child(5):checked ~ ul {
    transform: translateX(-400%);
}

/**
 * fade-in/scale effect
 */
.carousel-list li {
    opacity: .1;
    transition: all .4s;
    transform: scale(.1);
}

input:nth-child(1):checked ~ ul li:nth-child(1),
input:nth-child(2):checked ~ ul li:nth-child(2),
input:nth-child(3):checked ~ ul li:nth-child(3),
input:nth-child(4):checked ~ ul li:nth-child(4),
input:nth-child(5):checked ~ ul li:nth-child(5) {
    opacity: 1;
    transform: scale(1);
}

You can edit the width value below to test the "responsiveness" of this solution.

A more complex Carousel

How to display two items — separated by a gap — while only moving the carousel one item at a time.

.carousel {
    display: inline-block;
    width: 200px;
    padding-right: 190px; /* container width minus the 10px padding on the list items */
    overflow: hidden;
    border: 1px solid #ccc;
    border-radius: 3px;
    text-align: center;  /* to center the radio buttons */
}

.carousel-list {
    white-space: nowrap;
    padding: 0;
    margin: 0;
    border: none;
    transition: transform .3s;
}

.carousel-list li {
    white-space: normal; /* reset */
    display: inline-block;
    width: 100%;
    box-sizing: border-box;
    padding-right: 10px; /* to create the gap between the images */
}

.carousel-list img {
    width: 100%; /* fit the container */
    vertical-align: bottom;
}

.carousel input {
    margin-left: -3px;
}

.carousel input:nth-child(1):checked ~ ul {
    transform: translateX(0);
}

.carousel input:nth-child(2):checked ~ ul {
    transform: translateX(-100%);
}

.carousel input:nth-child(3):checked ~ ul {
    transform: translateX(-200%);
}

.carousel input:nth-child(4):checked ~ ul {
    transform: translateX(-300%);
}

.carousel input:nth-child(5):checked ~ ul {
    transform: translateX(-400%);
}

.carousel input:nth-child(6):checked ~ ul {
    transform: translateX(-500%);
}

.carousel input:nth-child(7):checked ~ ul {
    transform: translateX(-600%);
}