DEV Community

Cover image for Let's Make a 3D E-Card using only CSS!
Regis Gaughan, III
Regis Gaughan, III

Posted on

Let's Make a 3D E-Card using only CSS!

In my previous post, Let's Make it Snow!, I mentioned the reason for making the snow in the first place was to spice up a simple page to share some photos with family and friends.

What I didn't mention was that the photos were actually "stuffed inside" a 3D, interactive, holiday e-card, that was created using nothing but HTML & CSS. Come along with me on the journey of creating this.


Want instant gratification?
This post is written as a tutorial, building up our 3D card through iteration. If you'd like to skip all that, you can head down to the "final" CodePen immediately.

The Challenge

Most devs would probably approach the idea of any interactivity on a webpage with JavaScript first: Click a card, use JS to handle the event, and kickoff a bunch of eased animations to have that card open, etc. And none of that's wrong at all. JavaScript has a role, and it certainly would fit here.

But what if I told you we can make this interactive, immersive e-card using only HTML & CSS? So throw out the JavaScript, and don't you dare reach for some bloated framework! Let's get started.

The Tech & Approach

I always find it useful to talk through our approach before opening up an editor. For this, we'll have a card on the page with two states: It's either opened or it's closed, and when it changes we want to animate it from the old state to the new using CSS; but it's still just a binary state.

What element can you think of that also has an inherent binary state? Of course, it's the checkbox input! So, we'll use the checkbox: When it's checked we'll consider the card to be opened, and when it's unchecked we'll consider the card to be closed.

Plus, we don't need JavaScript to check and uncheck a checkbox!

"But how does a checkbox help us style the card to look open and closed?"

Good question. And CSS has the power to do this for us with ease!

First, let's understand that CSS will let us select a checkbox whose state is :checked and, separately, we can select a sibling of an element.

So, if we wanted the text of a label right after a checkbox to be green when checked and red when not, we could do something like this:

input[type="checkbox"] + label { color: red; }
input[type="checkbox"]:checked + label { color: green; }
Enter fullscreen mode Exit fullscreen mode
Style a label element that's next to a checkbox based on the checkbox's state.

Iā€™m going to show you how to use this pattern to style our card to be opened or closed based on the checkbox's state.

"But clicking on a tiny checkbox is cumbersome. Can't we use JavaScript to click the card itself?"

We could... or we could rely on plain old HTML! You see, a <label> element whose for attribute is a value that matches a checkbox input's id will toggle that checkbox checked state when the label is clicked.

<input type="checkbox" id="the-checkbox" />
<label for="the-checkbox">Click me!</label>
Enter fullscreen mode Exit fullscreen mode
In this example when we click the label the checkbox above will be toggled.

Therefore, our plan for our e-card will be that the card itself will be contained in a , so clicking on the card will toggle our checkbox's state between checked and unchecked, meaning we can then style our card to be open or closed. Even further, we can "hide" the checkbox itself, so clicking the label is the only visual way to open the card.

Now, on to the code!

Our markup will obviously be the checkbox toggle followed by our label and the associated for attribute as we talked about above.

The label element will act as our card container, which we'll apply some special CSS to for the 3D effect of opening later. It will also define our card's layout and dimensions.

Within the label we'll have three "card-faces" positioned on top of each other absolutely. We'll have the outside front-flap (the cover), the inside of he front-flap, and the inside of the back-flap. (We don't need a back-flap-outside because we won't be able to turn the card over.) These card-faces are what will be visually styled to be the faces of the card and all contained as siblings within our card container and animate with rotation transforms.

Our initial markup will look something like this:

<input type="checkbox" id="card-toggle" />
<label class="card" for="card-toggle">
  <div class="card-face front-flap outside">
  </div>
  <div class="card-face front-flap inside">
  </div>
  <div class="card-face back-flap inside">
  </div>
</label>
Enter fullscreen mode Exit fullscreen mode

Now let's style it!

I'm using Sass (SCSS) here but, remember, Sass is just a pre-processor and it's all just regular CSS in the end.

First up is our checkbox. We position this absolutely with a z-index of 1 so we can overlay our card label on top of it with a z-index of 2.

#card-toggle {
  position: absolute;
  z-index: 1;
  left: 50%;
}
Enter fullscreen mode Exit fullscreen mode

Next is our card--the label element. We're going to lay this out on the page and position it relatively on top of checkbox with z-index. Finally, we set a perspective so the children's rotation transforms will appear 3D; with the edge getting larger as it opens "closer" to users' eyes.

.card {
  // These just lay the card out on the page.
  display: block;
  margin: 100px auto;
  width: 600px;
  height: 400px;

  // Position this correctly on top of our card toggle.
  position: relative;
  z-index: 2;

  // This is what will make our card flaps feel 3D.
  perspective: 2000px;
}
Enter fullscreen mode Exit fullscreen mode

Now, our card faces. Remember from our markup above we have three card faces which are positioned absolutely on top of each other. Will also share a few more styles for each of them, like a base background color, and a pointer cursor so the user knows to click.

.card-face {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #FAFAFA;
  cursor: pointer;
  // ... continued below.
}
Enter fullscreen mode Exit fullscreen mode

These faces are the elements that will transform by rotating. The CSS default of a transform origin is in the center of the element--but that's not how cards open. So we set the transform origin to be anchored at the top/left.

We'll also be animating these, obviously. So we'll add our transition. I use all because we'll be animating a couple other properties too, like box-shadow, etc.

.card-face {
  // ...
  transform-origin: 0 0;
  transition: all 0.5s ease-out;
}
Enter fullscreen mode Exit fullscreen mode

Now that we have our base card-face defined, let's style the individual faces for their starting position.

The two card faces of the front flap share a class of front-flap and should always have the same transform, since they're "physically" the same flap (you can't flip the outside cover without also flipping the inside cover). But, the outside face will have a higher z-index than our inside, since it should appear on top.

When we "flip" the card open we also need the outside to hide itself, since it has a higher z-index it would show. So, we use the backface-visibility property so the outside face disappears when it's flipped around and our inside face will show instead.

.front-flap {
  // Start the front-flap a little open so we can see
  // the back-flap underneath.
  transform: rotateX(21deg);
  box-shadow: 0 10px 15px rgba(0, 0, 0, 0.15);
}

.front-flap.outside {
  // Modern Safari still requires the -webkit- prefix :\
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  z-index: 5;
}

.front-flap.inside {
  z-index: 4;
}
Enter fullscreen mode Exit fullscreen mode

And we only have one back-flap for the inside. It's z-index should be completely under the front-flap faces. (I've skipped z-index 3 below as we're going to use it later, so the back-flap gets 2 here):

.back-flap.inside {
  z-index: 2;
  // Start slightly open, so it feels a little more natural.
  transform: rotateX(-4deg);
  box-shadow: 0 0px 5px rgba(0,0,0,0.25), 0 5px 10px rgba(0,0,0,0.25);
}
Enter fullscreen mode Exit fullscreen mode

Alright, with everything above set up we should have a card that's just begging to be opened and look something like this:

Alt Text

Let's get it opened!

You'll notice that clicking on the card doesn't do anything. Well, that's not entirely true; clicking on the card does toggle the checkbox, which is hidden under the card visually. But now we'll style our card so when the checkbox is checked, our card opens.

We're going to use the :checked selector on our checkbox and the adjacent sibling combinator to target our card when the checkbox is checked.

To get the effect, we want to rotate both of the front-flap faces when the checkbox is checked. We'll also change our shadow since they're now upside-down.

#card-toggle:checked + .card .front-flap {
  transform: rotateX(165deg);
  box-shadow: 0 10px 15px rgba(0, 0, 0, 0.08), 5px 5px 5px rgba(0, 0, 0, 0.08), -5px 5px 5px rgba(0, 0, 0, 0.08);
}
Enter fullscreen mode Exit fullscreen mode

Now the card opens and closes when we click it! And that's our e-card. We can now add content to the cards, like a photo to the front cover, and a personal message to the back...

But, are we going to call it done after that? Nope!

Let's add something inside!

As I mentioned, I wanted to share some photos with family and friends. If I sent a real card I would have naturally put a few photos inside the card itself. Let's do it here as well!

We'll add some photos inside the card label element. Since everything is absolutely positioned they don't need to go anywhere specific. I've put them at the end:

<input type="checkbox" id="card-toggle" aria-label="Open the card">
<label class="card" for="card-toggle">
  <div class="card-face front-flap outside">
  </div>
  <div class="card-face front-flap inside">
  </div>
  <div class="card-face back-flap inside">
  </div>

  <!-- Photos that fall out. -->
  <figure class="photo"><img src="..." alt="..." /></figure>
  <figure class="photo"><img src="..." alt="..." /></figure>
  <figure class="photo"><img src="..." alt="..." /></figure>
  <figure class="photo"><img src="..." alt="..." /></figure>
</label>
Enter fullscreen mode Exit fullscreen mode

For the initial styles of these photos, we want them to be "inside" the card. That means they'll need to be laid out and positioned under the front-flaps z-indexes, as well as in the center of the closed card.

.photo {
  // Position them absolutely
  position: absolute;
  height: 225px;
  width: 300px;
  z-index: 3;
  top: 50px;

  // We use left: 50% and a negative margin-left so the
  // photos are aligned in the middle of the card itself.
  left: 50%;
  margin-left: -150px;

  // We don't want to be able to click the images, or they'll
  // also toggle the checkbox as children of the label.
  pointer-events: none;

  // Style to make them look a bit more realistic.
  box-shadow: 0 0 5px rgba(0,0,0,0.25), 0 5px 10px rgba(0,0,0,0.25);
  background: #fff;
  box-sizing: border-box;
  border: 4px solid #fff;
  border-radius: 1px;

  // Animate the photos. This is a base transition, we adjust some
  // of the first few to spend longer when animating out.
  transition: all .5s .06s ease-out;

  // Make sure the nested img element fits the view exactly
  > img {
    height: 100%;
    width: 100%;
    object-fit: cover;
  }
}
Enter fullscreen mode Exit fullscreen mode

And then, when the checkbox is checked, and the card is opening, let's move the photos out of the card:

#card-toggle:checked + .card .photo {
  &:nth-of-type(1) {transform: translate(-145px, 250px);}
  &:nth-of-type(2) {transform: translate(145px, 300px);}
  &:nth-of-type(3) {transform: translate(-145px, 480px);}
  &:nth-of-type(4) {transform: translate(145px, 530px);}
}

Enter fullscreen mode Exit fullscreen mode

We now have something like this.
Alt Text

This is OK, kinda. But we can still...

Go the extra mile!

So we have some photos falling out of the card but they don't really feel very natural yet. If we start them rotated when in the card so they rotate as they fall out, that should help. We can also have them be a little offset in the card as well:

.photo {
  // ...

  // To help with a bit of randomness to the start, we'll make
  // the photos initially rotated 90deg and shifted a bit over...
  transform: translate(50px, 0px) rotate(90deg);
  // ...and make every third photo the opposite.
  &:nth-of-type(3n) {
    transform: translate(-50px, 0px) rotate(-90deg);
  }
Enter fullscreen mode Exit fullscreen mode

Now let's add a small "random" rotation to the final positions as well as some "random" jitter to the x and y as well:

#card-toggle:checked + .card .photo {
  &:nth-of-type(1) {transform: translate(-149px, 248px) rotate(-3deg);}
  &:nth-of-type(2) {transform: translate(139px, 303px) rotate(2deg);}
  &:nth-of-type(3) {transform: translate(-142px, 475px) rotate(5deg);}
  &:nth-of-type(4) {transform: translate(146px, 534px) rotate(-2deg);}
}
Enter fullscreen mode Exit fullscreen mode

And here we go:
Alt Text

Much better!

There's one last thing we can do before I show you the final CodePen.

Make it mobile friendly!

Right now it's not very mobile friendly, especially if we want a fixed viewport. A few tweaks to make it mobile friendly would be to let the width of the card flex if we get narrow, and have the photos fall into a single stream. Here's how we would do that:

.card {
  // ...

  // Way back up in .card we'll change our width from a static
  // 600px to a high percentage, but with some space on the sides.
  // Then we'll give our card a max-width of 600px;
  width: 90%;
  max-width: 600px;
}

// Next, when we're too narrow for two columns of photos,
// make it one column, with some randomness applied:
@media (max-width: 860px) {
  #card-toggle:checked + .card .photo {
    &:nth-of-type(1) {transform: translate(2px, 248px) rotate(-3deg);}
    &:nth-of-type(2) {transform: translate(-4px, 478px) rotate(2deg);}
    &:nth-of-type(3) {transform: translate(-1px, 714px) rotate(5deg);}
    &:nth-of-type(4) {transform: translate(3px, 942px) rotate(-2deg);}
  }
}

// And, finally, when super narrow, make our photos narrower,
// and slightly modify where they start inside the card.
@media (max-width: 350px) {
.photo:nth-of-type(1n) {
  width: 200px;
  margin-left: -100px;
  transform: translate(0px, 0px) rotate(-90deg);
}
Enter fullscreen mode Exit fullscreen mode

It's the Final CodePen.

So I hope you taketake what you've learned and make it your own by add some content to the cover and inside, add more photos, etc.

Below you'll find my own CodePen which adds some more details, and uses some more advanced Sass loops to better randomize the photos falling out.

And, again, all this was done with just HTML & CSS, so don't let anyone ever tell you CSS isn't a programming language! šŸ™ƒ

Top comments (0)