How to Create a Flip Card Using CSS? (Learn 'Exactly' How It Works)
Tags: frontend, webdev, css, tutorial
Published: 2023-09-21 07:48:55

## Table of Contents<a name=""></a>

1. [Introduction](#Introduction)
2. [The HTML of the card](#The_HTML_of_the_card)
3. [Styling the card sides](#Styling_the_card_sides)
4. [Backface visibility](#Backface_visibility)
5. [Establishing a 3D rendering context](#Establishing_a_3D_rendering_context)
6. [The final transition](#The_final_transition)
7. [Improving the effect](#Improving_the_effect)
    1. [Adding depth using `perspective`](#Adding_depth_using_perspective)

    2. [Fixing the `:hover` issue](#Fixing_the_:hover_issue)





-----

## Introduction<a name="Introduction"></a>

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:

{% codepen https://codepen.io/codeguage-code/pen/eYbyEdP %}

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<a name="The_HTML_of_the_card"></a>

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](https://getbem.com/), in a slightly modified way.

Here's our simple and sleek HTML:

```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<a name="Styling_the_card_sides"></a>

_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:

```css
.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;
}
```

{% codepen https://codepen.io/codeguage-code/pen/oNJpwqJ %}


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:

```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:

{% codepen https://codepen.io/codeguage-code/pen/JjwMJme %}

_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:

```css
.card {
   /* border: 2px solid black; */
   width: 120px;
   height: 150px;
   position: relative;
}
```

And here's the output we get for it:

{% codepen https://codepen.io/codeguage-code/pen/BavJZGK %}

Let's move on.

-----

## Backface visibility<a name="Backface_visibility"></a>

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.

```css
.card_back {
   background-color: pink;
   transform: rotateY(180deg);
}
```

And here's the output we get for it:

{% codepen https://codepen.io/codeguage-code/pen/eYbyRQV %}

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:

```css
.card_front {
   background-color: yellow;
   z-index: 1;
}
```

And here's the output we get for it:

{% codepen https://codepen.io/codeguage-code/pen/GRPyEPg %}


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.

{% codepen https://codepen.io/codeguage-code/pen/vYvpZbB %}


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.

**📖 Further reading:**
To learn more about how `backface-visibility` works, refer to the following chapter from our CSS course: [CSS 3D Transformations — Backface Visibility](https://www.codeguage.com/courses/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<a name=""></a>
> 
> 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`:

```css
.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:

{% codepen https://codepen.io/codeguage-code/pen/qBLpjvg %}

Now, let's flip the `.card`, and see the output:

```css
.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:

{% codepen https://codepen.io/codeguage-code/pen/dywJRLB %}

_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<a name="Establishing_a_3D_rendering_context"></a>

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](https://www.codeguage.com/courses/css/3d-transformations-transform-style) 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:

```css
.card {
   width: 120px;
   height: 150px;
   position: relative;
   transform-style: preserve-3d;
}
```

{% codepen https://codepen.io/codeguage-code/pen/abPEwrQ %}

_Not anything new here._

Let's now see the card's flipped state, which is where our anticipation lies:

```css
.card {
   width: 120px;
   height: 150px;
   position: relative;
   transform-style: preserve-3d;
   transform: rotateY(180deg);
}
```

{% codepen https://codepen.io/codeguage-code/pen/mdapwZJ %}

_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<a name="The_final_transition"></a>

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:

```css
.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:

```css
.card:hover {
   transform: rotateY(180deg);
}
```

{% codepen https://codepen.io/codeguage-code/pen/eYbyEpj %}

-----

## Improving the effect<a name="Improving_the_effect"></a>

Technically, our flip card transition is done but there are two problems with it:

1. One is that there isn't any _depth_ in the rotation.

2. 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`<a name="Adding_depth_using_perspective"></a>

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:

```css
.card {
   width: 120px;
   height: 150px;
   position: relative;
   transform-style: preserve-3d;
   transition: 5s ease;
}
```

Hover over the following `<div>`:

{% codepen https://codepen.io/codeguage-code/pen/KKbZvVZ %}

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:

```css
.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.

**📖 Further reading:**
To understand `perspective()` in detail, refer to the chapter, [CSS 3D Transformations — Perspective](https://www.codeguage.com/courses/css/3d-transformations-perspective), from our CSS course.

And here's the output:

{% codepen https://codepen.io/codeguage-code/pen/qBLpXbz %}

_See? That's the depth we were talking about._

### Fixing the `:hover` issue<a name="Fixing_the_:hover_issue"></a>

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.

{% codepen https://codepen.io/codeguage-code/pen/qBLpXbz %}



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`**:

```html
<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:

```css
.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:

{% codepen https://codepen.io/codeguage-code/pen/xxmpLVe %}


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:

{% codepen https://codepen.io/codeguage-code/pen/eYbyEdP %}

_And this is our flip card effect._

-----

## More about CodeGuage

At CodeGuage, we offer absolutely free-of-cost courses on [CSS](https://www.codeguage.com/courses/css/), [JavaScript](https://www.codeguage.com/courses/js/), [Advanced JavaScript](https://www.codeguage.com/courses/advanced-js/), [AJAX](https://www.codeguage.com/courses/ajax/), and [React](https://www.codeguage.com/courses/react/), to get better at frontend development. In addition, many more courses are planned to be introduced in the near future.

You can [follow us on Twitter](https://twitter.com/codeguage) in order to stay updated with all of the latest endeavors happening on our website.

And if you enjoyed reading this article, don't forget to ❤ it and share it with others.