How to create a flip card using HTML and CSS?
Explore the elementary machinery behind implementing a 3D flip card transition using CSS.
Introduction
With the advent of CSS 3D transforms 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 plugins.
One such transition is the flip card transition, demonstrated below (hover over the card to see the effect):
In this article, you'll learn how to implement such a flip card transition purely using HTML and CSS. The tools we'll be using include perspective, rotateY(), backface-visibility, transform-style and obviously transition.
The card's HTML
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 call the container .card. The front side can be .card_front and the back side can 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 the sake of keeping things simple, I've put the text "Front" on the front side of the card and the text "Back" on the back side of the card. In a real setting, of course, you can have any content as you wish.
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 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 some starter CSS code:
.card {
border: 2px solid black;
}
.card_front,
.card_back {
height: 150px;
width: 120px;
/* My preferential styles */
border-radius: 6px;
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, I'll remove it.
Now, let's overlap the card sides on top of each other. The purpose of overlapping is quite obvious — it's basically just how a card is in reality.
For the overlap, we'll need absolute positioning. This further demands the container, .card, to have dimensions of its own, otherwise with the absolutely positioned sides, it'll collapse to a zero height. Adding together these technical requirements, let's make a simple change...
Let's put the width and height setting in .card and instead have the sides, .card_front and .card_back, consume all the available space with the help of width: 100% and height: 100%:
.card {
border: 2px solid black;
width: 120px;
height: 150px;
position: relative;
}
.card_front,
.card_back {
position: absolute;
height: 100%;
width: 100%;...
}As promised earlier, with the .card element styled correctly, we'll now remove the border style from it:
.card {
width: 120px;
height: 150px;
position: relative;
}Backface visibility
Currently, as you can see, the back side of the card (which comes later in the HTML source code) is visible while the front side is hidden behind it.
To emulate a real card, this back side needs to be sent to... you got it... the back. This can be achieved by rotating it around the y-axis by 180 degrees and one additional thing (which I'll get to later):
.card_back {
transform: rotateY(180deg);
}Notice how we don't need to use any application of perspective in order to rotate the back side. This is because we are only concerned with the actual rotation itself and not with a transition taking us to that rotated state.
Now let's talk about the "additional thing."
As you can see, rotating the back side merely flips it horizontally; it doesn't hide it to cause the front side to reveal. To remedy this, we might think of using the z-index property on the front side and get it stacked above the back side.
While, yes, z-index will get the front side to be shown and the back side hidden in this way, but 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 I mean by this; you don't obviously need to write the CSS shown here:
.card_front {
z-index: 1;
}With the z-index applied on the front side, and thus the front side stacked above the back side, it indeed becomes visible. But when we flip the whole card (by virtue of a 180 degrees rotation around the y-axis), we still see the front side:
.card {
transform: rotateY(180deg);
}To many, this might seem counter-intuitive — the back side is behind the card, so rotating the entire card by 180 degrees across the y-axis should expose the back side.
Here's the explanation for this 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.
Alright, so now that we understand that z-index is of no use to us, what else can we use? What we really need here is the backface-visibility property.
As the name suggests, backface-visibility specifies the visibility of the back side of an element. By default, it's set to the value visible which means that the back side is visible to us (appearing as a mirror image) when the element is rotated. But using the value hidden, we can specify the back side to be hidden instead.
In our case, we ought to set backface-visibility: hidden on both the card's front and back sides. Here's why:
- Initially, the back side is stacked on top of the front side, likewise, it'll be shown. But because it's rotated and, in effect, its backface is facing us,
backface-visibility: hiddencauses it to be hidden in this initial state of the card. - Similarly, when the card is rotated, the front side gets rotated as well, leading to its backface exposed to us.
backface-visibility: hiddenagain causes it to be hidden in this flipped state of the card.
backface-visibility on the front side
Strictly speaking, backface-visibility: hidden isn't required on the card's front side here. That's because, in the flipped state, when the card's back side shows up, due to its stacking in the z-axis (remember, .card_back appears later in the HTML source code), it already covers the front side behind it.
By using backface-visibility: hidden on the front side as well, we just make sure that if, for instance, the back side has a background color applied to it with an alpha filter value, we don't get to see the front side through it.
Here's the CSS obtained with backface-visibility's addition to .card_front and .card_back:
.card_front,
.card_back {
position: absolute;
height: 100%;
width: 100%;
backface-visibility: hidden;...
}Here's the output when the card isn't flipped:
Now, let's flip it and see the output:
.card {
transform: rotateY(180deg);
}What? We still don't get the correct output! Ideally, in this state, the back side should've been visible.
Why? That's because we are still missing one property on the .card element to get the same 3D space shared by .card, .card_front and .card_back. Can you guess which property is it?
Establishing a 3D rendering context
As you saw above, in the flipped state of the card, we still see the front side instead of the back one. Even backface-visibility didn't help us. Or did it?
Well it's not that backface-visibility isn't in action but rather that the rotation of the card itself doesn't constitute a rotation of its sides (.card_front and .card_back).
Think of .card_front and .card_back as embedded inside the .card element. When you rotate .card, you might assume that .card_front and .card_back rotate as well, but that's surprisingly NOT the case!
The card's front and back sides currently have their own 3D space. What we want here instead is for these sides and the card to share the same 3D space so that when we rotate the card, the sides get rotated as well.
This is accomplished using 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 with its children.
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:
.card {
width: 120px;
height: 150px;
position: relative;
transform-style: preserve-3d;
}Alright, let's test the output as before. First, for the card's initial state:
Not anything new here. And now for the card's flipped state, which is where all our anticipation lies:
.card {
transform: rotateY(180deg);
}Fabulous! The back side shows in the flipped state.
The last thing left now is to add a :hover transition to the card.
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, I add a 1s transition to the .card element with the ease transition function (which is the default):
.card {
width: 120px;
height: 150px;
position: relative;
transform-style: preserve-3d;
transition: 1s ease;
}The hover state should have .card rotated by 180 degrees. This is accomplished below:
.card:hover {
transform: rotateY(180deg);
}Hover over this card and witness 3D flipping goodness!
Improving the effect
Technically, we're done with our flip card transition but there are two minute problems with it:
- There is no depth in the rotation.
- The
:hoverstate is applied on the element which itself is rotated.
Let's fix both these issues to create a flawless flip card effect.
Adding depth using perspective
Let's slow down the transition above to be able to clearly visualize what I mean by there being no depth in the rotation. I'll increase the transition duration from 1s to 5s:
.card {
transition: 5s ease;
}Hover over the following card:
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 the rotation. And for this, we can use the perspective() transform function on the .card element, before the rotateY() function in the transform property, in its :hover state:
.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 the 3D effect.
Go on and hover over the card below and notice the change as it's rotated:
See? That's the depth I was talking about.
Fixing the :hover issue
With the same 5s transition in place, try hovering over the card shown below and then as it reaches its midway, take the mouse pointer very slightly out of it.
Notice one strange, albeit correct, thing as this happens. As soon as the pointer leaves the card in the middle of 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 same element 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 applying to the same element.
For this, let's create a wrapper around our .card element:
<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, moving the pointer out of its region (in the rotation) would not revert the transition unless we, of course, go outside the intial region of the card.
.card-cont {
display: inline-block;
}
.card-cont:hover .card {
transform: perspective(400px) rotateY(180deg);
}I've used display: inline-block on .card-cont so that it's only as big as the .card element.
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).
Before ending, let's revert back the card's transition duration to 1s:
.card {
transition: 1s ease;
}And that's it! A crisp and flawless flip card effect.