A recent project required a little extra 3D "pop". WebGL was experimented with, but it ended up being too heavy and not supported in enough browsers. Instead, we ended up going with a quick, 3D accordion effect accomplished entirely in CSS.

On mouse over, the thumbnail image folds up revealing metadata beneath. The effect is enhanced with special gradients and layers simulating lighting sources and shadows.

This article describes the technique and shows how it was implemented.

Example (Hover Over Image)

Implementation

To create the illusion that a single image is folding, we actually must create four different slices. Each slice has the same background image, but its position is offset so when laid out horizontally, they reproduce the original image.

Here's what each slice looks like:

HTML

In the HTML, we create our slices and add some wrapper elements.

<a href="" class="accordion_container">
  <div class="accordion_wrapper">
    <div class="panel panel1" style="background-color: red;">
      <div class="accordion_overlay"></div>
    </div>
    <div class="panel panel2" style="background-color: green;">
      <div class="accordion_overlay"></div>
    </div>
    <div class="panel panel3" style="background-color: blue;">
      <div class="accordion_overlay"></div>
    </div>
    <div class="panel panel4shadow"></div>
    <div class="panel panel4" style="background-color: purple;">
      <div class="accordion_overlay"></div>
    </div>
  </div>
</a>

CSS

Most of the action is in CSS, so I'll go over it piece by piece. Note: the source below is in a prefix-free syntax for clarity sake. In a real project, you'll need to setup the correct vendor prefixes.

First we setup are container size and style the background which will be revealed on hover:

.accordion_container {
  padding: 0;
  margin: 0;
  width: 308px;
  height: 172px; }
.accordion_wrapper {
  width: 306px;
  height: 172px;
  position: relative;
  background: linear-gradient(top, #535353, #373737);
  box-shadow: rgba(0, 0, 0, 0.1) -2px 2px 10px inset;
  perspective: 400px; }

Next we setup some generic styles and positions on each slice:

.panel {
  top: 0;
  bottom: 0;
  background: #eaeaea no-repeat 0 50%;
  position: absolute; 
  transition-duration: 100ms; }
.accordion_overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  transition: all 100ms linear;
  opacity: 0; }

Then we set specific sizes and positions on each slice. We're also setting up the gradients used for the fake lighting highlights:

.panel2, .panel3, .panel4, .panel4shadow {
  width: 68px; }
.panel1 {
  left: 0;
  width: 102px; }
  .panel1 .accordion_overlay {
    left: 50%;
    background: linear-gradient(left, rgba(0, 0, 0, 0), 
                                      rgba(0, 0, 0, 0.15)); }
.panel2 {  
  transform-origin: 0 0;
  left: 102px;
  background-position: -102px 50%; }
  .panel2 .accordion_overlay {
    background: linear-gradient(left, rgba(255, 255, 255, 0), 
                                      rgba(255, 255, 255, 0.4)); }
.panel3 {
  transform-origin: 68px 0;
  left: 170px;
  background-position: -170px 50%; }
  .panel3 .accordion_overlay {
    border-right: 1px solid black;
    background: linear-gradient(left, rgba(0, 0, 0, 0), 
                                      rgba(0, 0, 0, 0.2) 10%, 
                                      rgba(0, 0, 0, 0.7)); }
.panel4, .panel4shadow {
  transform-origin: 0 0;
  left: 238px;
  background-position: -238px 50%; }
  .panel4 .accordion_overlay {
    background: linear-gradient(right, rgba(0, 0, 0, 0), 
                                       rgba(0, 0, 0, 0.6)); }

These are the transitions returning from a hover a event:

.accordion_container .panel2 {
  transform: rotateY(0deg);
  transition-property: transform;
  transition-timing-function: linear; }
.accordion_container .panel3 {
  transform: translateX(0) rotateY(0);
  transition-property: transform;
  transition-timing-function: cubic-bezier(0.25, 0.45, 0.65, 1); }
.accordion_container .panel4 {
  transform: translateX(0) rotateY(0);
  transition-property: transform;
  transition-timing-function: cubic-bezier(0.25, 0.45, 0.65, 1); }
.accordion_container .panel4shadow {
  opacity: 0;
  background: rgba(0,0,0,0.3);
  transform: translateX(0) rotateY(0);
  transition-property: transform;
  transition-timing-function: cubic-bezier(0.25, 0.45, 0.65, 1); }

Finally, we setup the transitions to take place on hover:

.accordion_container:hover .panel .accordion_overlay {
  opacity: 1; }
.accordion_container:hover .panel2 {
  transform: rotateY(-70deg);
  transition-property: transform;
  transition-timing-function: cubic-bezier(0, 0.05, 1, 1); }
.accordion_container:hover .panel3 {
  transform: translateX(-90px) rotateY(70deg);
  transition-property: transform;
  transition-timing-function: cubic-bezier(0.42, 0, 1, 1); }
.accordion_container:hover .panel4 {
  transform: translateX(-90px) rotateY(-70deg);
  transition-property: transform;
  transition-timing-function: cubic-bezier(0.42, 0, 1, 1); }
.accordion_container:hover .panel4shadow {
  opacity: 1;
  transform: translateX(-84px) rotateY(-70deg);
  transition-property: transform, opacity;
  transition-timing-function: cubic-bezier(0.42, 0, 1, 1); }

Manual Timing Functions

The most important part of the illusion it to make it appear as if the image is folding. If our slices animate out of sync, gaps and breaks will appear between them, shattering the illusion.

We could write complex Javascript to calculate the correct 3D rotation and position throughout the animation, but since we're doing CSS-only, we have to fake it.

The transition-timing-function: cubic-bezier option allows use to manually time each transition. I slowed the animation down, by setting the duration to 5000ms, and slowly tweaked each slice's timing until they animated without gaps. Lots of trial and error, but the result is fantastic.