Flashcards in CSS Explained

Background

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.

Code Snippet

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;
}

CSS Explained

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).

Try it out

You can play around with the code snippet here