Animation, when used with restraint, can be a great way to enhance a user experience by making elements on the page feel more tangible and by adding visual interest. In this short tutorial, we'll use HTML, CSS, and just a touch of JavaScript to animate a card flipping over.
We'll be building up to the example in this CodeSandbox. Clicking the "Flip Card" button will show the opposite side of the card and do so with a nice, natural looking flip.
You can follow along with live examples embedded right in the article at the original source.
Initial Setup
Our card will consist of a pair of div
s, we'll toggle between which div
we want to show using a single CSS class, and we'll do the rest using CSS transforms. Let's setup our div
s and initial CSS to get started.
Markup
<body>
<div class="container">
<button type="button" id="flip-btn">Flip Card</button>
<div class="card">
<div id="back" class="cardBack">Back</div>
<div id="front" class="cardFront">Front</div>
</div>
</div>
</body>
Initial Styles
Let's start by giving our elements their basic styling.
Container
This element will wrap the entire example, so we'll make it take up the whole view port and throw a light gray background on it. We'll also give it a little padding, make it a flex container, set our flex direction to columns, and center everything.
.container {
padding: 20px;
background-color: lightgray;
display: flex;
flex-direction: column;
align-items: center;
margin: auto;
min-height: 100vh;
}
Card Wrapper
Looking at the markup, we can see that this div wraps the two div
s that represent the front and back of our card. We'll set a width, minimum height, and a position: relative
. We'll ultimately end up setting our front and back card divs to use position: absolute
. By putting the relative
here, we make sure that absolute positioning is applied relative to this div
.
.card {
margin-top: 1rem;
min-height: 300px;
width: 250px;
position: relative;
border-radius: 0.25rem;
}
Front and Back Cards
The front and back card divs will share quite a bit of their styling, so let's start with the shared styling. We're setting the box-sizing property to border-box
to make sure that however we style the cards, their width and height includes any borders or padding and they fit within the parent element's width. We'll also round the corners and apply a box shadow to provide a bit of depth.
.cardFront,
.cardBack {
box-sizing: border-box;
border-radius: 0.25rem;
height: 100%;
padding-left: 0.5rem;
padding-right: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
width: 100%;
}
Toggle Classes with JavaScript
We don't need a framework or build tools here, in fact, there's such a small amount of JavaScript here that we'll just add it in script tags right in the HTML file. Of course, you can make this happen in your favorite framework. For this demo, let's keep it simple and add the following script tags to your HTML file. I added mine right after the closing div for our container
.
<script>
const front = document.getElementById('front')
const back = document.getElementById('back')
const btn = document.getElementById('flip-btn')
function handleFlip() {
front.classList.toggle('flipped')
back.classList.toggle('flipped')
}
btn.addEventListener('click', handleFlip)
</script>
We're going to use the flipped
class on both front and back div
s and we'll control the styling through that. This code uses browser APIs to add and remove the flipped
class to the div
s when the button is clicked.
Transform Cards Based on The flipped
Class
Now that we have the flipped
class being toggled on our div
s, let's add some styles that are specific to the front and back cards, accounting for the flipped and non-flipped states.
Card Back Styling
We'll give the back card a background color, and then we'll use a transform to set an initial rotation. We're turning the card 180deg
on the Y
axis, flipping it over by default. Then when the flipped
class is applied, we use the same rotateY
function and flip the card to face us.
.cardBack {
transform: rotateY(180deg);
background-color: #ebf4ff;
}
.cardBack.flipped {
transform: rotateY(0deg);
}
Front Card Styling
We'll apply the same flipping behavior to the front card, but in reverse. We'll default to the card facing us, and rotate it 180deg
when the flipped
class is applied.
.cardFront {
transform: rotateY(0deg);
}
.cardFront.flipped {
transform: rotateY(180deg);
}
Add the Flip with CSS Transitions
With our logic in place to toggle our classes, and our transforms applied, we can observe the card sections applying the updated styles and "flipping". It's quite abrupt in its current state, so let's add a transition rule to our shared card styles. We'll add transition: transform 0.5s ease;
to transition our tranforms
over half a second, and we'll apply the ease
timing function.
.cardFront,
.cardBack {
box-sizing: border-box;
border-radius: 0.25rem;
height: 100%;
padding-left: 0.5rem;
padding-right: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
width: 100%;
+ transition: transform 0.5s ease;
}
With that applied, we have a nice animated transition when flipped.
Position Cards
Now we should position the cards so they behave like a single card instead of two stacked div
s. Our parent container has a position: relative
rule applied to it, so all we need to do is set a rule for each card element to position: absolute
. Now our cards will be absolutely positioned, relative to the parent container. Our card pieces are sitting on top of one another now.
.cardFront,
.cardBack {
box-sizing: border-box;
border-radius: 0.25rem;
height: 100%;
padding-left: 0.5rem;
padding-right: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
width: 100%;
transition: transform 0.5s ease;
+ position: absolute;
}
Hide the Back Side of Cards
When we flip our cards, we can still see both labels. This is because we only have a background color set on the back card, and the stacking order puts the front card on top, so we see both labels in their respective position and rotation based on that state of the flipped
class.
Let's update our styling to hide the back side of the cards so we only see the card that is face up. We'll do this by adding a backface-visibility
rule to our shared styles:
.cardFront,
.cardBack {
box-sizing: border-box;
border-radius: 0.25rem;
height: 100%;
padding-left: 0.5rem;
padding-right: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
width: 100%;
transition: transform 0.5s ease;
position: absolute;
+ backface-visibility: hidden;
}
Now when we flip the cards, we only see the face up card. Notice that when we can see the front card, we lose the background color that we had applied to the back card because it is hidden. We'll add a background to the front card in a future, cleanup step. For now we'll keep it so everything is clear in the UI as we progress.
We're almost done. The flip animation probably looks a little strange and unnatural at this stage. That's okay, we'll address that next.
Fix the Unnatural Flip
Perspective
To make this animation feel more natural, we'll apply some perspective to give the card a feeling of depth. I've settled on 600px
here, but feel free to play around with the values and pick something that feels "right" to you.
.cardBack {
- transform: rotateY(180deg);
+ transform: perspective(600px) rotateY(180deg);
background-color: #ebf4ff;
}
.cardBack.flipped {
- transform: rotateY(0deg);
+ transform: perspective(600px) rotateY(0deg);
}
.cardFront {
- transform: rotateY(0deg);
+ transform: perspective(600px) rotateY(0deg);
}
.cardFront.flipped {
- transform: rotateY(180deg);
+ transform: perspective(600px) rotateY(180deg);
}
Rotation Direction
Something is still off, our card flip still doesn't feel right. The remaining problem is the direction the cards flip in. They're essentially crashing through each other to swap rotations. We need them to rotate as a single unit, so we'll have to make one of the cards go in the opposite direction.
As it turns out, the transition algorithm will do some math and you can manipulate that math to control the direction the object moves in when transitioning. All we need to do is change our front card from 180deg
when flipped to -180deg
. It doesn't change the end result, but it does change the way the transition is applied.
.cardFront.flipped {
- transform: perspective(600px) rotateY(180deg);
+ transform: perspective(600px) rotateY(-180deg);
}
Final cleanup
All we have left is to apply a background color to the front card so our card isn't transparent from the front. You can do whatever you want in terms of color, background image, etc. Here we'll keep it simple and just apply the same background to both cards by moving our background-color
rule from the cardBack
-specific rules to our shared card styles.
.cardFront,
.cardBack {
box-sizing: border-box;
border-radius: 0.25rem;
height: 100%;
padding-left: 0.5rem;
padding-right: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
width: 100%;
transition: transform 0.5s ease;
position: absolute;
backface-visibility: hidden;
+ background-color: #ebf4ff;
}
.cardBack {
- background-color: #ebf4ff;
transform: perspective(600px) rotateY(180deg);
}
Conclusion
This is a pretty common interaction to animate and CSS makes it fairly simple to achieve. No need to complicate things with too much JavaScript. By using CSS for this, all the tricky logic and things we might have otherwise missed are handled for us by the browser. For example, if we toggle the flipped
class part way through a transition, it naturally changes direction without any jarring flickers or jumps. It just picks up the new value calculations from where you left off. Go ahead, click the flip buttons a whole bunch of times really fast... the card will flip flop and finish as expected without awkwardly jumping to the far end of the animation at any point. This is absolutely something that we could build with JavaScript, but not necessarily something we should build with JavaScript. By using a CSS based approach, we cut down our initial work, reduce the need for maintenance, and gain some performance by letting the browser handle this for us.
BTW, If you like to learn with bite-sized videos, I published this very thing as a lesson on egghead.io a while back, check it out!
Top comments (0)