Creating a 3D Cube Image Gallery

Guest Author //

The following is a guest post by Kushagra Gour (@chinchang457). Kushagra wrote to me to show me a fun interactive demo he made. It touches on many of the concepts of 3D transforms in CSS, a topic we haven't covered a ton here. So here's Kushagra taking the reins to explain these concepts through a demo.

I recently redesigned my website and came up with a 2-face 3D cube idea for the homepage and header. On hovering it rotates between my display pic and Twitter link. While doing so I thought why not extend the idea into a full 6-face cube which could be used as an image gallery. Here is what I came up with!

See the Pen 3D cube image gallery by Kushagra Gour (@chinchang) on CodePen

This tutorial outlines how you can make something like this, emphasizing the CSS3 3D concepts.

Breaking the cube

It is quite evident by seeing the cube that the 6 faces of the cube would be 6 different HTML elements. Six <div> elements in this case. Because they need to be rotated as one cube, they need to be in a container element. If we code this basic structure, we would have something like this:

<div class="cube">
    <div class="cube-face"></div>
    <div class="cube-face"></div>
    <div class="cube-face"></div>
    <div class="cube-face"></div>
    <div class="cube-face"></div>
    <div class="cube-face"></div>
 </div>

Also since we will need to reference each sides to style them, we should add appropriate classes to them.

<div class="cube">
    <div class="cube-face  cube-face-front"></div>
    <div class="cube-face  cube-face-back"></div>
    <div class="cube-face  cube-face-left"></div>
    <div class="cube-face  cube-face-right"></div>
    <div class="cube-face  cube-face-top"></div>
    <div class="cube-face  cube-face-bottom"></div>
 </div>

Styling the faces

We don't see anything yet. So lets give some dimension and style to the faces.

$size: 150px; // cube length
.cube {
  width: $size;
  height: $size;
  position: relative;
}
.cube-face {
  width: inherit;
  height: inherit;
  position: absolute;
  background: red;
  opacity: 0.5;
}

See the Pen 3d cube gallery tutorial - P1 by Kushagra Gour (@chinchang) on CodePen

Note that every cube face has position set to absolute so they stack upon each other at one place. Now we can select each and position accordingly.

Also I have given opacity to each face so we can see through them and see what's happening.

CSS3 3D concepts

Lets get to know some concepts of CSS3 3D. To bring the front face a little closer to the eyes, we translate it on the Z-axis:

.cube-face-front {
  color: blue;
  transform: translate3d(0, 0, 20px);
}

You won't see any difference yet. Lets understand why.

perspective

As mentioned on MDN:

The perspective CSS property determines the distance between the z=0 plane and the user in order to give to the 3D-positioned element some perspective.

In simple terms, its value determines the amount of 3D-ness in the space. The lesser the value of this property, more profound the 3D effect is. Without this property, the elements are rendered on the screen using Parallel projection in which the projection lines are parallel to each other. Therefore no matter how much closer an element move towards/away from you, it will still appear to be of the same size unlike in real world. (More information on perspective).

We'll set this property on the parent container of our cube so that all its children (faces) are affected by a common perspective, like so:

.cube {
  width: $size;
  height: $size;
  position: relative;
  
  perspective: 600px;
}

As expected, now the front face does appear bigger in size. But still something is missing.

transform-style

Even after we give perspective to our scene, we still have an issue. The front face should ideally appear above all other faces, hiding them behind it. But it's not.

The reason is that our cube container has no 3D rendering context which is defined as follows on CSSWG:

A containing block hierarchy of one or more levels, instantiated by elements with a computed value for the ‘transform-style’ property of ‘preserve-3d’, whose elements share a common three-dimensional coordinate system.

Without an element having the transform-style property set as preserve-3d, its children are rendered as flattened, having no stacking context. Thus even when we bought the front face closer on Z-axis, it continued to render it at its original z-index with no consideration to its position in the 3D space.

Try to give the .cube element this property and see what happens.

.cube {
  width: $size;
  height: $size;
  position: relative;
  
  perspective: 600px;
  transform-style: preserve-3d;
}

It worked. Now that we have our 3D system setup, let transform this into a cube!

Positioning the faces

We'll take one face at a time and place it at its appropriate position. First let's understand the coordinate system in CSS. If we were to take one of our cube face, it is something like this:

Front face lying flat with Z-axis coming towards us

As you can see, the Z-axis is coming out of the screen straight from the element. Hence, when we translate the front face positively on the Z-axis, it appears closer to us. A point to note here is that this coordinate system is local to this element. Let's look at that more closely.

We'll use our front face again and rotate it a bit about its Y-axis.

.cube-face-front {
  transform: rotateY(40deg);
}

Now this is how our front face looks like after the rotation:

Axes rotate with the face.

Note how the axes rotated along with element. This means that Z-axis is no longer coming straight towards us. Instead it is in the direction of the element. So if were to move it along the Z-axis now, it would move in the direction in which the element is facing.

This is the concept we'll be using to position every face of our cube. Assume that the center of the cube is in the 2D place of the screen. The cube faces then need to be positioned around it in the 3D space. Remember, We rotate the face so that it faces the required direction then translate on Z-axis.

Front face

Nothing to rotate here. Simple move the front face forward by half the cube's length.

.cube-face-front {
  transform: translate3d(0, 0, $size/2);
}

Back face

The back face will face in the opposite direction to the front one. Which means it needs to be rotated by 180 degrees about the Y-axis before translating like so:

.cube-face-back {
  transform: rotateY(180deg) translate3d(0, 0, $size/2);
}

2 sides done. 4 more to go.

Left face

In case you still have doubt how the transformation are being done, lets understand this face through some visuals.

This is how the left face is right now, lying flat in the 2D plane (z=0):

Left face: in the 2d plane

As the left side needs to face towards the left, we give it a rotation of 90 degrees:

Left face: after rotation

And as with every face, we move it on its Z-axis:

Left face: after translation

This is final CSS for left face:

.cube-face-left {
  transform: rotateY(-90deg) translate3d(0, 0, $size/2);
}

Right face

This is similar to the left face, except for a positive rotation:

.cube-face-right {
  transform: rotateY(90deg) translate3d(0, 0, $size/2);
}

Top face

This face needs to be rotated about the X-axis by 90 degrees so that it faces upwards:

.cube-face-top {
  transform: rotateX(90deg) translate3d(0, 0, $size/2);
}

Bottom face

Similarly, by giving a negative rotation we position the bottom face:

.cube-face-bottom {
  transform: rotateX(-90deg) translate3d(0, 0, $size/2);
}

This completes the positioning of the faces and now we have our cube complete with the following final CSS (I have also added random colors to each face to differentiate them):

$size: 150px; // cube length
.cube {
  margin: 100px;
  width: $size;
  height: $size;
  position: relative;
  
  perspective: 600px;
  transform-style: preserve-3d;
}
.cube-face {
  width: inherit;
  height: inherit;
  position: absolute;
  background: red;
  opacity: 0.8;
}
.cube-face-front {
  background: yellow;
  transform: translate3d(0, 0, $size/2);
} 
.cube-face-back {
  background: orange;
  transform: rotateY(180deg) translate3d(0, 0, $size/2);
} 
.cube-face-left {
  background: green;
  transform: rotateY(-90deg) translate3d(0, 0, $size/2);
} 
.cube-face-right {
  background: magenta;
  transform: rotateY(90deg) translate3d(0, 0, $size/2);
} 
.cube-face-top {
  background: blue;
  transform: rotateX(90deg) translate3d(0, 0, $size/2);
} 
.cube-face-bottom {
  background: red;
  transform: rotateX(-90deg) translate3d(0, 0, $size/2);
}

Now to rotate the cube we can simply apply rotations on the .cube element. Try giving it a rotation of 180 degrees around Y-axis (vertically):

.cube {
  margin: 100px;
  width: $size;
  height: $size;
  position: relative;
  
  perspective: 600px;
  transform-style: preserve-3d;
  transform: rotateY(180deg);
}

You should have something like:

See the Pen 3d cube gallery tutorial - P2 by Kushagra Gour (@chinchang) on CodePen

Do you notice something wrong? We turned the cube 180 degrees around its vertical axis. We should have seen the back face instead of the front face now. We do see it, but it is showing smaller for some reason. What did we do wrong?

Fixing the perspective

If you remember, we gave the perspective property to the cube container (.cube). And when we rotated that element just now, the perspective marked by the vanishing point also rotated along with it. So the vanishing point which was initially somewhere behind the 2D place came in front of the 2D plane after the rotation, causing the issue.

What we ideally want is that the perspective never changes and remains constant no matter what element we transform.

How do we fix this? We wrap all our elements with another DIV to which give the perspective property:

<div class="scene">
  <div class="cube">
    <div class="cube-face  cube-face-front"></div>
    <div class="cube-face  cube-face-back"></div>
    <div class="cube-face  cube-face-left"></div>
    <div class="cube-face  cube-face-right"></div>
    <div class="cube-face  cube-face-top"></div>
    <div class="cube-face  cube-face-bottom"></div>
  </div>
</div>
.scene {
  margin: 100px;
  width: $size;
  height: $size;
  
  perspective: 600px;
}
.cube {
  position: relative;
  width: inherit;
  height: inherit;
  
  transform-style: preserve-3d;
  transform: rotateY(180deg);
}

Check the result now and everything should appear as expected.

Try giving it different rotations like transform: rotateX(30deg) rotateY(30deg) to play with it little. Once done, remove the transform property.

Adding interactivity

We now add some controls to navigate through the gallery. For this we are going to use a nice trick called the Checkbox Hack. Though we'll be using radio buttons (as only one will be selected at a time) instead of checkboxes, the concept remains the same. You can read more about the Checkbox Hack in Chris Coyier's article.

Not going in much depth we add the radio buttons to our HTML:

<!-- CONTROLS -->      
<input type="radio" checked id="radio-front" name="select-face"/>    
<input type="radio" id="radio-back" name="select-face"/>
<input type="radio" id="radio-left" name="select-face"/>
<input type="radio" id="radio-right" name="select-face"/>
<input type="radio" id="radio-top" name="select-face"/>
<input type="radio" id="radio-bottom" name="select-face"/>
<div class="scene">
  <div class="cube">
    <div class="cube-face  cube-face-front"></div>
    <div class="cube-face  cube-face-back"></div>
    <div class="cube-face  cube-face-left"></div>
    <div class="cube-face  cube-face-right"></div>
    <div class="cube-face  cube-face-top"></div>
    <div class="cube-face  cube-face-bottom"></div>
  </div>
</div>

and following CSS to bind the cube's rotation with the radio buttons:

#radio-back:checked ~ .scene .cube {
  transform: rotateY(180deg);
} 
#radio-left:checked ~ .scene .cube {
  transform: rotateY(90deg);
} 
#radio-right:checked ~ .scene .cube {
  transform: rotateY(-90deg);
}
#radio-top:checked ~ .scene .cube {
  transform: rotateX(-90deg);
}  
#radio-bottom:checked ~ .scene .cube {
  transform: rotateX(90deg);
}

In the above CSS we simply state when each radio button is checked, what should be the rotation of the cube at that time.

Final Code

To make it more pleasing, we add some transition effect to the cube and proper alignment to get the following code:

<!-- CONTROLS -->
<input type="radio" checked id="radio-front" name="select-face"/>    
<input type="radio" id="radio-left" name="select-face"/>
<input type="radio" id="radio-right" name="select-face"/>
<input type="radio" id="radio-top" name="select-face"/>
<input type="radio" id="radio-bottom" name="select-face"/>
<input type="radio" id="radio-back" name="select-face"/>

<div></div><!-- separator -->

<div class="scene">
  <div class="cube">
      <div class="cube-face  cube-face-front"></div>
      <div class="cube-face  cube-face-back"></div>
      <div class="cube-face  cube-face-left"></div>
      <div class="cube-face  cube-face-right"></div>
      <div class="cube-face  cube-face-top"></div>
      <div class="cube-face  cube-face-bottom"></div>
   </div>
</div>
$size: 150px; // cube length
body {
  text-align: center;
  padding: 50px;
} 
.scene {
  display: inline-block;
  margin-top: 50px;
  width: $size;
  height: $size;
  
  perspective: 600px;
}
.cube {
  position: relative;
  width: inherit;
  height: inherit;
  
  transform-style: preserve-3d;
  transition: transform 0.6s;
}
.cube-face {
  width: inherit;
  height: inherit;
  position: absolute;
  background: red;
  opacity: 0.8;
}
// faces
.cube-face-front {
  background: yellow;
  transform: translate3d(0, 0, $size/2);
}  
.cube-face-back {
  background: black;
  transform: rotateY(180deg) translate3d(0, 0, $size/2);
} 
.cube-face-left {
  background: green;
  transform: rotateY(-90deg) translate3d(0, 0, $size/2);
} 
.cube-face-right {
  background: magenta;
  transform: rotateY(90deg) translate3d(0, 0, $size/2);
} 
.cube-face-top {
  background: blue;
  transform: rotateX(90deg) translate3d(0, 0, $size/2);
} 
.cube-face-bottom {
  background: red;
  transform: rotateX(-90deg) translate3d(0, 0, $size/2);
}  
// controls 
#radio-back:checked ~ .scene .cube {
  transform: rotateY(180deg); 
} 
#radio-left:checked ~ .scene .cube {
  transform: rotateY(90deg); 
} 
#radio-right:checked ~ .scene .cube {
  transform: rotateY(-90deg); 
}
#radio-top:checked ~ .scene .cube {
  transform: rotateX(-90deg); 
}  
#radio-bottom:checked ~ .scene .cube {
  transform: rotateX(90deg); 
}

Adding some background-image to all the faces we get the final result:

See the Pen 3D cube image gallery by Kushagra Gour (@chinchang) on CodePen

Hope you enjoyed this 3D ride and create some really amazing things using it!