I was trying to create a simple flashcard app recently in order to build a quick tool to help myself study a bit more efficiently. I wasn't sure how to achieve the cool flip animation so I did some research and came across an excellent article that described how to do this perfectly in React. I didn't follow that approach exactly and ended up using an additional library called CSSTransition. I'm writing this blog post so that someone new to CSS and this library can also easily see why certain CSS lines were included in this simple example.
So to start, let's describe the code sample we're dealing with. Here's the correspoding React snippet:
const nodeRef = useRef(null);
return (
<CSSTransition
classNames={{
enter: styles.cardCommonEnter,
enterActive: styles.cardCommonEnterActive,
exit: styles.cardCommonExit,
exitActive: styles.cardCommonExitActive,
}}
in={flipState}
nodeRef={nodeRef}
timeout={3000}>
<div
className={styles.cardCommon}
ref={nodeRef}
onClick={() => {setFlipState(!flipState)}}>
<div className={styles.cardFront}>
{FLASHCARD_CONTENT[props.index].question}
</div>
<div className={styles.cardBack}>
{FLASHCARD_CONTENT[props.index].answer}
</div>
</div>
</CSSTransition>
);
And here's the CSS file:
.cardCommon {
cursor: pointer;
background-color: blue;
justify-content: center;
align-items: center;
display: flex;
height: 150px;
width: 180px;
transform-style: preserve-3d;
}
.cardFront, .cardBack {
position: absolute;
padding: 1rem;
backface-visibility: hidden;
}
.cardBack {
transform: rotateX(180deg);
}
.cardCommonEnter {
transform: perspective(1000px) rotateX(0);
}
.cardCommonEnterActive {
transform: perspective(1000px) rotateX(180deg);
transition: 500ms;
}
.cardCommonExit {
transform: perspective(1000px) rotateX(180deg);
}
.cardCommonExitActive {
transform: perspective(1000px) rotateX(0deg);
transition: 500ms;
}
The base React code is relatively straightforward, so I won't spend too much time explaining what's happening here. Let me instead dig into the CSS and CSSTransition pieces since that's what I found hard to fully grok.
To start with, we have a simple .cardCommon
section. This tells the CSS to apply that style to the div
that has className={styles.cardCommon}
to it. And similarly .cardFront, .cardBack
says to apply
this CSS style to cardFront
and cardBack
elements.
Next we'll see that the .cardFront, .cardNack
section has a position: absolute
. This is key
because without this, both elements are laid out as normal (as in they will both
take up space when rendering). This is actually something that we don't want in this case, we want
both of these components to stack on top of one another, hence we use position: absolute
so they're both
positioned in terms of their nearest ancestor (which happens to be card
). You'll also notice that we have
backface-visibility: hidden
for this section and this is to ensure that you can't see the back of any
component. We also see that we have a .cardBack
section which has a transform: rotateY(180deg)
section
telling it to turn 180 degrees. Note that this tells the cardBack
component to turn around (but doesn't apply
to the front). This is the very important to having the two components feel like they're both written
on two faces of the same card.
Now, onto the React code. CSSTransition
applies additional animations to a specified element which is passed as a reference in the prop nodeRef
. In our code snippet, you can see that nodeRef
is used as a variable to tie it to the div
right below CSSTransition
.
This code triggers animations when the in
prop transitions state. So when in
is set to true
, it starts animating nodeRef
to first immediately go to the enter
state (in our case apply the style .cardCommonEnter
) and then transition immediately to .cardCommonEnterActive
.
The user sees this slowly because you'll see that .cardCommonEnterActive
has a transition
section telling it to be applied over 500ms.
We also see exit transitions trigger when the in
prop transitions from true
to false
. Except here we see the exit
states being activated (.cardCommonExit
and .cardCommonExitActive
).
You can play around with the code snippet here