Introduction
With the advent of CSS 3D transformations in the mid 2000s, a great number of 3D transitions became possible on the web without having to pull up tedious codes utilizing WebGL or some old-school graphics plugin.
One such transition is the flip card transition, demonstrated below:
In this article, we shall understand how exactly to implement a flip card transition using just pure HTML and CSS. The tools we'll be using include perspective
, rotateY()
, backface-visibility
, transform-style
and obviously transition
.
This how-to guide is not like any other guide, where you're just given a code snippet to follow; you'll instead get to understand each and every aspect of implementing a 3D flip card transition.
The HTML of the card
First things first, we have to set up the HTML for the flip card.
Here's an overview of the elements we need:
- An element representing the front side of the card.
- An element representing the back side of the card.
- A container element holding the above two elements.
Let's write the HTML for these elements.
We'll call the container .card
. The front side will be .card_front
and the back side will be .card_back
. This nomenclature follows the BEM naming convention, in a slightly modified way.
Here's our simple and sleek HTML:
<div class="card">
<div class="card_front">Front</div>
<div class="card_back">Back</div>
</div>
For now, for the sake of keeping things simple, we've put the text 'Front' on the front side of the card and the text 'Back' on the back side of the card.
Now, let's move to the CSS part, starting with making both .card_front
and .card_back
look like the sides of a card.
Styling the card sides
It's impossible to have a card with two differently-sized sides, right?
Likewise, the very first thing we'll do in the CSS is to style the front and back sides of the cards identically.
Here's the CSS we get thus far:
.card {
border: 2px solid black;
}
.card_front,
.card_back {
height: 150px;
width: 120px;
border-radius: 6px;
/* Just styling the text nicely */
line-height: 150px;
font-family: sans-serif;
font-size: 20px;
text-align: center;
}
.card_front {
background-color: yellow;
}
.card_back {
background-color: pink;
}
Notice the border applied to .card
— this is temporary and only meant to see how much space the .card
element spans. Once we're done with this section, we'll remove this rather bogus style.
Now, let's overlap the card sides on top of each other.
The purpose of overlapping is quite obvious — in a real card, the sides are clearly one top of each other, and so to create a card graphically, we ought to do the same thing.
For the overlap, we'll shift the absolute width and height settings to .card
, and instead get the card sides to fill up the width and height of the parent .card
container.
Here's the new CSS:
.card {
border: 2px solid black;
width: 120px;
height: 150px;
position: relative;
}
.card_front,
.card_back {
position: absolute;
height: 100%;
width: 100%;
border-radius: 6px;
/* Just styling the text nicely */
line-height: 150px;
font-family: sans-serif;
font-size: 20px;
text-align: center;
}
And here's the output we get for it:
Looks like a decent card, doesn't it?
As promised, with the <.card>
element correctly styled, we'll now remove the border
style from it:
.card {
border: 2px solid black;
width: 120px;
height: 150px;
position: relative;
}
And here's the output we get for it:
Let's move on.
Backface visibility
Currently, as you can see, the backside of the card (which comes later in the HTML source code) is being displayed while the frontside is hidden behind it.
In a real card, as we know, the backside is rotated by 180 degrees around the y-axis (if we dissect the card). So first off we need to rotate the backside and then do another additional thing.
Let's perform the rotation.
.card_back {
background-color: pink;
transform: rotateY(180deg);
}
And here's the output we get for it:
Notice how we don't need to use any application of perspective in order to rotate the backside. This is because we are only concerned with the actual rotation itself and not with a transition taking us to that rotated state.
Now over to the additional thing.
Initially, we ideally want to hide the backside and only show the frontside of the card. To do so, we might think of using the z-index
property on the frontside and thus get it stacked above the backside.
While, yes, z-index
will get the frontside to be shown and the backside hidden, it isn't going to give us the correct result in the longer run, once we flip the card.
Following is a quick demonstration of what we mean by this; you don't obviously need to write the CSS shown here:
.card_front {
background-color: yellow;
z-index: 1;
}
And here's the output we get for it:
With the z-index
applied on the frontside, and thus the frontside stacked above the backside, we do see the backside initially.
But when we flip the whole card, by virtue of 180 degrees rotation around the y-axis, we still see the frontside.
To many, this might seem counter-intuitive and rightly so — the backside is behind the card so rotating the entire card by 180 degrees should expose the backside.
Here's why we get this weird but correct behavior:
If an element A is stored above another element B, then no matter which kind of rotation we do, the stacking will remain the same. For CSS engines, it's easier and practical to manage elements on a webpage this way rather than changing the stacking order with every 3D rotation transformation.
What we really need here is the backface-visibility
property.
As the name suggests, backface-visibility
specifies the visibility of the backside of an element.
By default, it's set to the value visible
which means that the backside is visible to us (appearing as a mirror image). But using the value hidden
, we can specify the backside to be hidden instead.
backface-visibility
works, refer to the following chapter from our CSS course: CSS 3D Transformations — Backface Visibility.In our case, we'll set backface-visibility: hidden
on both the card's frontside and the backside.
Here's why:
Initially, the backside is stacked on top of the frontside, likewise, it'll be shown. But because it's rotated and, in effect, its backside is facing us, we can use backface-visibility: hidden
to hide it in this initial state.
Similarly, when the card is rotated, the frontside gets rotated as well, leading to its backface exposed to us. Using backface-visibility: hidden
again, we can hide it in this flipped state of the card.
Simple.
backface-visibility
on the frontside
Note that, strictly speaking, backface-visibility: hidden
isn't required on the card's frontside.
That's because, in the flipped state, when the card's backside shows up, due to its stacking in the z-axis (remember it appears later in the HTML source code), it covers the frontside behind it.
By using backface-visibility: hidden
on the frontside as well, we just make sure that if, for instance, the backside has a background color applied to it with an alpha opacity value, we don't get to see the frontside through it.
Here's the code we get with the addition of backface-visibility
to .card_front
and .card_back
:
.card_front,
.card_back {
position: absolute;
height: 100%;
width: 100%;
border-radius: 6px;
backface-visibility: hidden;
/* Just styling the text nicely */
line-height: 150px;
font-family: sans-serif;
font-size: 20px;
text-align: center;
}
And here's the output we get for it, supposing that .card
isn't rotated:
Now, let's flip the .card
, and see the output:
.card {
width: 120px;
height: 150px;
position: relative;
transform: rotateY(180deg);
}
And here's the output we get for it, supposing that .card
isn't rotated:
What? We still don't get the correct output!
Well, that's because we are missing one important property from the .card
element, to get the same 3D space to be shared by .card
, .card_front
and .card_back
— the transform-style
property.
Establishing a 3D rendering context
In the code above, when we rotate the .card
element by 180 degrees, we don't get the desired flipped state, where the card's backside is shown to us.
This is not because backface-visibility
isn't in action but rather because the rotation of the card doesn't constitute a rotation of .card_front
and .card_back
.
Currently, .card_front
and .card_back
could be thought of as embedded inside the .card
element. So when .card
is rotated, the rendering seems as if .card_front
or .card_back
have been rotated themselves, but that's surprisingly NOT the case.
The card's front and back sides are rendered into the plane of .card
, each having its own 3D space.
What we want here is for the card and its sides to share the same 3D space so that when we rotate the card, the sides get rotated themselves as well.
If you've read the chapter CSS 3D Transformations — The transform-style
Property from our CSS course, you'll know how to do this. That is, we need the transform-style
property.
transform-style
, set to the value preserve-3d
, gets the underlying element to establish a 3D rendering context. This effectively results in the elements to share a common 3D space.
For our case, we want .card
, .card_front
and .card_back
to share the same 3D space. Hence, we'll set transform-style: preserve-3d
on the .card
element.
Let's do this now and see the result it gives to the card's initial state:
.card {
width: 120px;
height: 150px;
position: relative;
transform-style: preserve-3d;
}
Not anything new here.
Let's now see the card's flipped state, which is where our anticipation lies:
.card {
width: 120px;
height: 150px;
position: relative;
transform-style: preserve-3d;
transform: rotateY(180deg);
}
Perfect! The backside shows in the flipped card.
The final thing left to do now is to add a :hover
transition to the card, flipping it using a 3D rotation.
The final transition
When we hover over the card, we want it to be flipped smoothly. Clearly, this means to use transition
.
In the following code, we add a 1s transition to the .card
element, with the default ease
transition function:
.card {
width: 120px;
height: 150px;
position: relative;
transform-style: preserve-3d;
transition: 1s ease;
}
The hover state should have the .card
rotated by 180 degrees:
This is accomplished below:
.card:hover {
transform: rotateY(180deg);
}
Improving the effect
Technically, our flip card transition is done but there are two problems with it:
- One is that there isn't any depth in the rotation.
- The other is the fact that the
:hover
state is applied on the element which itself is rotated.
Let's fix both these issues and create a flawless flip card effect.
Adding depth using perspective
Let's slow down the transition above to be able to clearly visualize what we mean by there being no depth in our rotation.
We'll change the transition duration from 1s to 5s:
.card {
width: 120px;
height: 150px;
position: relative;
transform-style: preserve-3d;
transition: 5s ease;
}
Hover over the following <div>
:
As you can see, the rotation appears completely flat; it does not give us the impression of being in a 3D setting.
To alleviate this issue, we just need to add perspective to bring depth into our rotation effect.
For this, we'll use the perspective()
transform function on the .card
element, before the rotateY()
function in the transform
property in its :hover
state.
Here's the new code we get:
.card:hover {
transform: perspective(400px) rotateY(180deg);
}
The argument provided to perspective()
specifies the distance between the view point and the screen (sometimes referred to as the z=0 plane). The larger the distance, the less pronounced is the 3D effect.
perspective()
in detail, refer to the chapter, CSS 3D Transformations — Perspective, from our CSS course.And here's the output:s
See? That's the depth we were talking about.
Fixing the :hover
issue
With the same slow card transition in place, try hovering over the card and then as it reaches its midway, take the mouse pointer out of it.
Notice one strange, albeit correct, thing as this happens. As soon as the pointer leaves the card amid its rotated state, the transition gets reverted back.
This is simply because the :hover
state is applied to the .card
element and it's also the .card
that gets rotated. With the rotation, the interactive region of the card changes and, thus, as we move the pointer out of the card (in this rotating transition), it counts as leaving the card.
To alleviate this issue, we need to separate the :hover
state and the rotation from the same element.
For this, we'll create a wrapper around our .card
element; let's call it .card-cont
:
<div class="card-cont">
<div class="card">
<div class="card_front">Front</div>
<div class="card_back">Back</div>
</div>
</div>
The :hover
state will now be attached to .card-cont
while the rotation will be performed, as before, on .card
.
In this way, while the card is rotating, and we move the pointer out of its region (in the rotation), the transition would continue happening since the pointer would still be inside .card-cont
and :hover
is attached to .card-cont
.
Following is the CSS code:
.card-cont {
display: inline-block;
}
.card-cont:hover .card {
transform: perspective(400px) rotateY(180deg);
}
The display: inline-block
style is given to get the .card-cont
element to fit the .card
element and not span the entire available width.
Let's see the output:
From a distant view, this small change allows us to prevent seemingly-snappy flip card effects (where the pointer's movement amid the card's transition is the real culprit).
To end with, let's revert back our transition duration to 1s to get our nice and quick flip card effect:
And this is our flip card effect.