How to Make a CSS Animated Greeting Card 💌

July 02, 2020

Recently, I wrote about how I made a card for a baby shower with CSS.

Today I’ll go through the steps of how to build a greeting card using CSS animations, so that you too can wow your friends or family with a super personalized, eco-friendly and homemade card made with love and dev skills straight from the heart. ♥️

The end result will look similar to this:

As a note: when I’m working in CodePen, I turn on Autoprefixer to handle any needed vendor-prefixes for cross-browser compatibility. I also use Sass as a CSS pre-processor. For simplicity’s sake, I won’t be including those in this tutorial.

1. The Markup

The HTML for the card itself is pretty simple. We’ve got a handful of divs representing the card and each side of it you will see: the front, inside left and inside right. We also provide a text instruction for how to interact with the card.

You can add in your own personal messages inside the .card__panel divs. Since most store-bought cards contain a message on the front and inside right of the card, I left comments in these locations. But this is your card, add a message wherever you’d like!

  <p>Click card to open</p>

  <div class="card__container js-card-opener">
    <div class="card">
      <div class="card__panel card__panel--front">
        <!-- Your front of card message goes here -->
      </div>
      <div class="card__panel card__panel--inside-left">
      </div>
      <div class="card__panel card__panel--inside-right">
        <!-- Your inside card message goes here -->
      </div>
    </div>
  </div>

2. Initial styles to make it look like a card

Now let’s add in some CSS to make these divs look like an actual greeting card.

Define the card dimensions

A physical greeting card is typically 4x6 inches, so we’ll maintain that same aspect ratio.

400 x 600 pixels looks pretty good on a desktop screen, but what about other screen sizes? Chances are good your recipient may be viewing the card on their phone and we want to make sure this thing scales down nicely.

We can go ahead and set max dimensions, so the browser knows not to let our card get bigger than 400 x 600px.

.card {
  max-width: 400px;
  max-height: 600px;
}

So how do we handle scaling? With viewport units!

If you’re unfamiliar with viewport units, 1vw is equal to 1% of the width of the viewport. Meaning, the element will scale relative to its viewport. This is different than assigning a percentage like 1% because that will be relative to the element’s parent size, which in our case will not be the same size as the viewport.

We want the card to take up 80% of the viewport on a smaller screen, so we’ll assign 80vw as the width. In order to maintain the 4x6 aspect ratio, we’ll use the following formula to calculate the aspect ratio:

(original height / original width) * new width = new height

Plugging in our actual numbers (6 / 4) * 80 = 120 we can assign 120vw to the height.

.card {
  max-width: 400px;
  max-height: 600px;
  width: 80vw;
  height: 120vw;
}

Why didn’t we use vh for the height? Since 120vh is equal to 120% of the viewport height, our card would overflow off the screen. Also, because we’re trying to maintain an aspect ratio with our dimensions, we want both height and width to respond to the same scale, which in this case is the viewport width.

Centering the card

There are many ways to do this now, but since we’ll be animating the positioning of the card when it opens and have variable width and height, the position absolute and transform method works well.

We’ll also go ahead and apply cursor:pointer now for a nice UX benefit; soon our container will be a clickable element.

.card__container {
  cursor: pointer;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

Styling each side of the card

The front, inner left and inner right sides of the card all need some of the same layout styles that can be targeted with a shared class rule.

.card__panel {
  border: 1px solid black;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
}

For applying specific styles for each panel, we’ll target the modifier classes below.

The important thing to note here is the assignment of z-index properties to each panel in the order in which they should be stacked. (Background colors are just for demo purposes, feel free to get creative with these!)

.card__panel--front {
  background: #6288e6;
  z-index: 1;
}
.card__panel--inside-left {
  background: #fff;
  z-index: 0;
}
.card__panel--inside-right {
  border-left: none;
  background: #fff;
  z-index: -1;
}

Give it some depth

To create the perception that this card is laying down on a flat surface, like a real card might, we can rotate the X axis of the .card using the transform rotateX property. By itself, this style will rotate the card, but it will appear flat and a bit weird.

To fix this, we apply perspective to the .card__container to create a more three-dimensional appearance.

We also want to make sure all children of our card are positioned in the same 3D space as our rotated div. The transform-style property set to preserve-3d ensures this is the case.

.card__container {
  perspective: 1400px;
}
.card {
  transform: rotateX(65deg);
  transform-style: preserve-3d;
}

All of this is optional and comes with a caveat – applying the perspective property creates a new stacking context and the element becomes a containing block for any position: fixed children. Depending on what you put inside your card, it could cause some undesired effects. (I ran into this with my baby shower card and chose not to rotate that card in the end.)

Putting it all together

Here are all of the styles for our initial greeting card layout. We’re also creating the foundation for our animation to open the card.

body {
  background: gray;
  text-align: center;
}
.card__container {
  cursor: pointer;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  perspective: 1400px;
}
.card {
  max-width: 400px;
  max-height: 600px;
  width: 80vw;
  height: 120vw;
  margin: auto;
  transform: rotateX(65deg);
  transform-style: preserve-3d;
}
.card__panel {
  border: 1px solid black;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
}
.card__panel--front {
  background: #6288e6;
  z-index: 1;
}
.card__panel--inside-left {
  background: #fff;
  z-index: 0;
}
.card__panel--inside-right {
  border-left: none;
  background: #fff;
  z-index: -1;
}

3. A tiny bit of JavaScript to open and close the card

The card should open and close when the user clicks on the .card__container div. That animation will be completely CSS-driven, but we do need to use JavaScript to toggle a class on the body to distinguish between the open and closed states of the card.

const openBtn = document.querySelector(".js-card-opener");

openBtn.onclick = function () {
  document.body.classList.toggle("open");
};

At this point, you should be able to see the body .open class toggle in dev tools when clicking on the card, but nothing else will be happening.

4. Layer in that CSS animation magic

Now for the fun stuff! When the user clicks on the card we want it to open of course, but we also need it to do a few other things.

Re-position on open

On a desktop screen, we want the card to be centered when it opens. The open state will be twice as wide as the closed card because we’ll be seeing both the inner left and right panels, so we need to adjust our positioning.

However, on a narrower screen (ex: phone, tablet) we do not want to center the card. There’s just not enough screen space to view both sides of the card. Since the inside right of the card is likely where the message will be, we want to make sure that’s readable.

For that reason, we’ll target this re-positioning on open with a media query.

  .card__container {
    transition: all 0.2s ease;
  }
  @media (min-width: 768px) {
    .open .card__container {
      transform: translate(0%, -50%);
    }
  }

Tip: to better understand this problem, try removing the media query and scaling your browser down to 375px. You’ll likely see why we don’t want to re-position on a smaller screen.

Stand that card up

If our card is laying down, we want it to stand up when it opens. The following two lines of CSS takes care of that.

.card {
  transition: all 1s ease;
}
.open .card {
  transform: rotateX(0deg);
}

Finally! Rotate the card open

To open the card, we will use the rotate3d transform function to transition our front and inside left panels -170 degrees around the Y-axis when the card opens. The inside right panel does not move since that’s the part we want to be revealed by the animation.

The front panel also gets backface-visibility set to hidden, while the inside panels are set to visible. This prevents the user from seeing the backside of the front of the card when it opens because it has a higher z-index than the inner panels.

.card__panel {
  transition: all 1s ease;
  backface-visibility: visible;
  transform-origin: left;
  transform-style: preserve-3D;
  transform: rotate3d(0, 1, 0, 0deg);
}
.card__panel--front {
  backface-visibility: hidden;
}
.open .card__panel--front {
  transform: rotate3d(0, 1, 0, -170deg);
}
.open .card__panel--inside-left {
  transform: rotate3d(0, 1, 0, -170deg);
}

Adding these into our existing styles

Combined with our initial styles, here’s the full CSS output:

body {
  text-align: center;
  background: gray;
}
.card__container {
  cursor: pointer;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  perspective: 1400px;
  transition: all 0.2s ease;
}
@media (min-width: 768px) {
  .open .card__container {
    transform: translate(0%, -50%);
  }
}
.card {
  max-width: 400px;
  max-height: 600px;
  width: 80vw;
  height: 120vw;
  transform-style: preserve-3d;
  transform: rotateX(65deg);
  transition: all 1s ease;
}
.open .card {
  transform: rotateX(0deg);
}
.card__panel {
  border: 1px solid black;
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
  transition: all 1s ease;
  backface-visibility: visible;
  transform-origin: left;
  transform-style: preserve-3D;
  transform: rotate3d(0, 1, 0, 0deg);
}
.card__panel--front {
  backface-visibility: hidden;
  background: #6288e6;
  z-index: 1;
}
.open .card__panel--front {
  transform: rotate3d(0, 1, 0, -170deg);
}
.card__panel--inside-left {
  background: #fff;
  z-index: 0;
}
.open .card__panel--inside-left {
  transform: rotate3d(0, 1, 0, -170deg);
}
.card__panel--inside-right {
  border-left: none;
  background: #fff;
  z-index: -1;
}

That's it!

And there you have it. Check out the final CodePen with all the pieces put together. Feel free to fork this pen or dig around in the code.

If you make your own CSS greeting card, I’d love to see what you create. Drop me a note on Twitter or CodePen.


Tammy Ritterskamp

Hey there! I'm a front-end web developer living and working in St. Louis, MO. Let's connect in these social places: