Cara menggunakan css slide toggle animation

You know how you can get cardboard boxes that come totally flat? You fold ‘em up and tape ‘em to make them into a useful box. Then when it’s time to recycle them, you cut them back apart to flatten them. Recently, someone reached out to me about essentially this concept as a 3D animation and I thought it would make an interesting tutorial to do it entirely in CSS, so here we are!

How might that animation look? How could we create that packing timeline? Could the sizing be flexible? Let’s make a pure CSS package toggle.

Here’s what we’re working towards. Tap to pack and unpack the cardboard box.

CodePen Embed Fallback

Where to start?

Where do you even start with something like this? It’s best to plan ahead. We know we’re going to have a template for our package. And that will need folding up in three dimensions. If working with 3D in CSS is new to you, I recommend this article to get you started.

If you’re familiar with 3D CSS, it might be tempting to construct a cuboid and go from there. But, that’s going to pose some problems. We need to consider how a package goes from 2D to 3D.

Let’s start by creating a template. We need to plan ahead with our markup and think about how we want our packing animation to work. Let’s start with some HTML.

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Mixins are a good idea

There’s quite a bit happening there. It’s a lot of

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
6s. I often like to use Pug for generating markup so I can split things up into reusable blocks. For example, every side will have two flaps. We can create a Pug mixin for the sides and use attributes to apply a modifier class name to make all that markup a lot easier to write.

mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom
      
mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")

We’re using two mixins. One creates the flaps for each side of the box. The other creates the sides of the box. Notice in the

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
7 mixin we are making use of
<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
8. That is where children of mixin usage get rendered which is particularly useful, as we need to nest some of the sides to make our lives easier later.

Our generated markup:

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Nesting the sides

Nesting the sides makes it easier to fold up our package. Much like each side has two flaps. The children of a side can inherit the sides’ transform and then apply their own. If we started with a cuboid, it would be hard to leverage this.

Cara menggunakan css slide toggle animation

Check out this demo that flips between nested and non-nested elements to see the difference in action.

CodePen Embed Fallback

Each box has a

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
9 set to the bottom right corner with
<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
0. Checking the “Transform” toggle rotates each box
<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
1. But, see how the behavior of that
<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
2 changes if we nest the elements.

We’re flipping between the two versions of markup but not changing anything else.

Nested:

<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>

Not nested:

<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>

Transforming all the things

After applying some styles to our HTML, we have our package template.

CodePen Embed Fallback

The styles specify the different colors and position the sides to the package. Each side gets a position that’s relative to the “main” side. (You’ll see why all that nesting is useful in a moment.)

There are some things to be aware of. Much like working with cuboids, we are using

<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
3,
<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
4, and
<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
5 variables for sizing. This will make it easier to change our package sizing down the line.

.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}

Why define sizing like this? We are using a unit-less default sizing of

<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
6, an idea I picked up from Lea Verou’s 2016 CSS ConfAsia talk (starting at 52:44). Using custom properties as “data” instead of “values,” we are free to do what we want with them using 
<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
7. Additionally, JavaScript doesn’t have to care about value units and we can change to pixels, a percentage, etc., without having to make changes elsewhere. You could refactor this into a coefficient in the 
<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
8, but it could also quickly become overkill.

The flaps for each side also need a size ever so smaller than the sides they are a part of. This is so we can see a slight gap as we would in real life. Also, the flaps on two sides need to sit a little lower. This is so that when we fold them up, we don’t get

<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
9 fighting between them.

.package__flap {
  width: 99.5%;
  height: 49.5%;
  background: var(--flap-bg, var(--face-4));
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0);
}
.package__flap--top {
  transform-origin: 50% 100%;
  bottom: 100%;
}
.package__flap--bottom {
  top: 100%;
  transform-origin: 50% 0%;
}
.package__side--extra > .package__flap--bottom,
.package__side--tabbed > .package__flap--bottom {
  top: 99%;
}
.package__side--extra > .package__flap--top,
.package__side--tabbed > .package__flap--top {
  bottom: 99%;
}

We’re also starting to consider the

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
9 for the individual pieces. A top flap will rotate from its bottom edge and a bottom flap will rotate from its top edge.

We can use a pseudo-element for the tab on that right side. We are using

<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
1 to get that desired shape.

.package__side--tabbed:after {
  content: '';
  position: absolute;
  left: 99.5%;
  height: 100%;
  width: 10%;
  background: var(--face-3);
  -webkit-clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  transform-origin: 0% 50%;
}

Let’s start working with our template on a 3D plane. We can start by rotating the

<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
2 on the X and Y axis.

.scene {
  transform: rotateX(-24deg) rotateY(-32deg) rotateX(90deg);
}

CodePen Embed Fallback

Folding up

We’re ready to start folding up our template! Our template will fold up based on a custom property,

<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
3. If the value is
<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
4, then we can fold up the template. For example, let’s fold some of the sides and the pseudo-element tab.

.package__side--tabbed,
.package__side--tabbed:after {
  transform: rotateY(calc(var(--packaged, 0) * -90deg)); 
}
.package__side--extra {
  transform: rotateY(calc(var(--packaged, 0) * 90deg));
}

CodePen Embed Fallback

Or, we could write a rule for all sides that aren’t the “main” one.

mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom
      
mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")
0

And that would cover all the sides.

CodePen Embed Fallback

Remember when I said the nested sides allow us to inherit a parent’s transform? If we update our demo so we can change the value of

<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
3, we can see how the value affects the transforms. Try sliding the
<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
3 value between
<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
4 and
<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
8 and you’ll see exactly what I mean.

CodePen Embed Fallback

Now that we have a way to toggle the folding state of our template, we can start working on some motion. Our previous demo flips between the two states. We can make use of

<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
9 for that. The quickest way? Add a
<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
9 to the
<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
2 of every child in the
<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
2.

mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom
      
mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")
1

CodePen Embed Fallback

Multi-step transitions!

But we don’t fold the template all up in one go — in real life, there’s a sequence to it where we’d fold up one side and its flap first then move on to the next, and so on. Scoped custom properties are perfect for this.

mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom
      
mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")
2

Here we are saying that, for each

<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
9, use a
.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}
4 of
.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}
5 multiplied by
.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}
6. The
.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}
6 value won’t change but each element can define which “step” it is in the sequence. And then we can be explicit about the order in which things happen.

mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom
      
mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")
3

Consider the following demo for a better idea of how this works. Change the slider values to update the order in which things happen. Can you change which car wins?

CodePen Embed Fallback

That same technique is key for what we are going to for. We could even introduce an

.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}
8 that adds a slight pause to everything for even more realism.

mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom
      
mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")
4

CodePen Embed Fallback

If we look back at our package, we can take this further and apply a “step” to all the elements that are going to

<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
2. It’s quite verbose but it does the job. Alternatively, you could inline these values in the markup.

mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom
      
mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")
5

CodePen Embed Fallback

But, it doesn’t feel very realistic.

Maybe we oughta flip the box, too

If I were folding up the box in real life, I’d likely flip the box up before folding in the top flaps. So how might we do that? Well, those with an eager eye might have noticed the

.package__flap {
  width: 99.5%;
  height: 49.5%;
  background: var(--flap-bg, var(--face-4));
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0);
}
.package__flap--top {
  transform-origin: 50% 100%;
  bottom: 100%;
}
.package__flap--bottom {
  top: 100%;
  transform-origin: 50% 0%;
}
.package__side--extra > .package__flap--bottom,
.package__side--tabbed > .package__flap--bottom {
  top: 99%;
}
.package__side--extra > .package__flap--top,
.package__side--tabbed > .package__flap--top {
  bottom: 99%;
}
0 element. We are going to use this to slide the package. Then we’re going to rotate the package on the x-axis. This will create the impression of flipping the package onto its side.

mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom
      
mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")
6

Adjusting the

.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}
5 declarations accordingly gives us something like this.

CodePen Embed Fallback

Unfolding the box

If you flip between the folded and not folded states, you’ll notice that the unfold doesn’t look right. The unfolding sequence should be the exact reverse of the folding sequence. We could flip the

.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}
5 based on
<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
3 and the number of steps. Our latest step is
.package__flap {
  width: 99.5%;
  height: 49.5%;
  background: var(--flap-bg, var(--face-4));
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0);
}
.package__flap--top {
  transform-origin: 50% 100%;
  bottom: 100%;
}
.package__flap--bottom {
  top: 100%;
  transform-origin: 50% 0%;
}
.package__side--extra > .package__flap--bottom,
.package__side--tabbed > .package__flap--bottom {
  top: 99%;
}
.package__side--extra > .package__flap--top,
.package__side--tabbed > .package__flap--top {
  bottom: 99%;
}
4. We can update our
<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
9 to this:

mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom
      
mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")
7

That is quite the mouthful of

.package__flap {
  width: 99.5%;
  height: 49.5%;
  background: var(--flap-bg, var(--face-4));
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0);
}
.package__flap--top {
  transform-origin: 50% 100%;
  bottom: 100%;
}
.package__flap--bottom {
  top: 100%;
  transform-origin: 50% 0%;
}
.package__side--extra > .package__flap--bottom,
.package__side--tabbed > .package__flap--bottom {
  top: 99%;
}
.package__side--extra > .package__flap--top,
.package__side--tabbed > .package__flap--top {
  bottom: 99%;
}
6 to reverse the
.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}
4. But, it works! We must remind ourselves to keep that
.package__flap {
  width: 99.5%;
  height: 49.5%;
  background: var(--flap-bg, var(--face-4));
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0);
}
.package__flap--top {
  transform-origin: 50% 100%;
  bottom: 100%;
}
.package__flap--bottom {
  top: 100%;
  transform-origin: 50% 0%;
}
.package__side--extra > .package__flap--bottom,
.package__side--tabbed > .package__flap--bottom {
  top: 99%;
}
.package__side--extra > .package__flap--top,
.package__side--tabbed > .package__flap--top {
  bottom: 99%;
}
8 value up to date though!

CodePen Embed Fallback

We do have another option. As we continue down the “pure CSS” route, we will eventually make use of the checkbox hack to toggling between the folding states. We could have two sets of defined “steps” where one set is active when our checkbox gets checked. It’s certainly a more verbose solution. But, it does give us more finite control.

mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom
      
mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")
8

Sizing and centering

Before we ditch the use of

.package__flap {
  width: 99.5%;
  height: 49.5%;
  background: var(--flap-bg, var(--face-4));
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0);
}
.package__flap--top {
  transform-origin: 50% 100%;
  bottom: 100%;
}
.package__flap--bottom {
  top: 100%;
  transform-origin: 50% 0%;
}
.package__side--extra > .package__flap--bottom,
.package__side--tabbed > .package__flap--bottom {
  top: 99%;
}
.package__side--extra > .package__flap--top,
.package__side--tabbed > .package__flap--top {
  bottom: 99%;
}
9 in our demo, let’s have a play with the size of our package. We want to check that our package remains centered while folding and flipping. In this demo, the package has a larger
<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
3 and the
<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
2 has a dashed border.

CodePen Embed Fallback

We may as well tweak our

<div class="boxes boxes--nested">
  <div class="box">
    <div class="box">
      <div class="box">
        <div class="box"></div>
      </div>
    </div>
  </div>
</div>
2 to better center the package while we’re at it:

mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom
      
mixin side()
  .package__side(class=`package__side--${attributes.class || 'side'}`)
    +flaps()
    if block
      block

.scene
  .package__wrapper
    .package
      +side()(class="main")
        +side()(class="tabbed")
        +side()(class="extra")
          +side()(class="flipped")
9

This gives us reliable centering in the scene. It all comes down to preference though!

CodePen Embed Fallback

Adding in the checkbox hack

Now let’s get

.package__side--tabbed:after {
  content: '';
  position: absolute;
  left: 99.5%;
  height: 100%;
  width: 10%;
  background: var(--face-3);
  -webkit-clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  transform-origin: 0% 50%;
}
3 out of the way and make this “pure” CSS. For this, we need to introduce a bunch of controls in the HTML. We are going to use a checkbox for folding and unfolding our package. Then we’re going to use a
.package__side--tabbed:after {
  content: '';
  position: absolute;
  left: 99.5%;
  height: 100%;
  width: 10%;
  background: var(--face-3);
  -webkit-clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  transform-origin: 0% 50%;
}
4 button to pick a package size.

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
0

In the final demo, we will hide the inputs and make use of the label elements. For now, though, let’s leave them all visible. The trick is to use the sibling combinator (

.package__side--tabbed:after {
  content: '';
  position: absolute;
  left: 99.5%;
  height: 100%;
  width: 10%;
  background: var(--face-3);
  -webkit-clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  transform-origin: 0% 50%;
}
5) when certain controls get
.package__side--tabbed:after {
  content: '';
  position: absolute;
  left: 99.5%;
  height: 100%;
  width: 10%;
  background: var(--face-3);
  -webkit-clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  transform-origin: 0% 50%;
}
6. We can then set custom property values on the
<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
2.

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
1

And here is the demo with that working!

CodePen Embed Fallback

Final polish

Now we’re in a place to make things look “pretty” and add some extra touches. Let’s start by hiding all the inputs.

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
2

We can style the sizing options as rounded buttons:

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
3

We want to be able to tap anywhere to toggle between folding and unfolding our package. So our

.package__side--tabbed:after {
  content: '';
  position: absolute;
  left: 99.5%;
  height: 100%;
  width: 10%;
  background: var(--face-3);
  -webkit-clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  transform-origin: 0% 50%;
}
8 and
.package__side--tabbed:after {
  content: '';
  position: absolute;
  left: 99.5%;
  height: 100%;
  width: 10%;
  background: var(--face-3);
  -webkit-clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  transform-origin: 0% 50%;
}
9 labels will take up the full screen. Wondering why we have two labels? It’s a little trick. If we use a
.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}
4 and scale up the appropriate label, we can hide both labels while the package transitions. This is how we combat spam tapping (even though it won’t stop a user hitting the space bar on a keyboard).

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
4

Check out this demo to see where we’ve added

.scene {
  transform: rotateX(-24deg) rotateY(-32deg) rotateX(90deg);
}
1 to both
.package__side--tabbed:after {
  content: '';
  position: absolute;
  left: 99.5%;
  height: 100%;
  width: 10%;
  background: var(--face-3);
  -webkit-clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  transform-origin: 0% 50%;
}
8 and
.package__side--tabbed:after {
  content: '';
  position: absolute;
  left: 99.5%;
  height: 100%;
  width: 10%;
  background: var(--face-3);
  -webkit-clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  clip-path: polygon(0 0%, 100% 20%, 100% 80%, 0 100%);
  transform-origin: 0% 50%;
}
9. Neither label is visible during the transition.

CodePen Embed Fallback

We’ve got complete functionality! But, our package is a little underwhelming at the moment. Let’s add extra details to make things more “box”-like with things like parcel tape and packing labels.

CodePen Embed Fallback

Little details like this are only limited by our imagination! We can use our

<div class="boxes">
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
  <div class="box"></div>
</div>
3 custom property to affect anything. For example, the
.scene {
  transform: rotateX(-24deg) rotateY(-32deg) rotateX(90deg);
}
5 is transitioning the
.scene {
  transform: rotateX(-24deg) rotateY(-32deg) rotateX(90deg);
}
6 transform:

<div class="scene">
  <div class="package__wrapper">
    <div class="package">
      <div class="package__side package__side--main">
        <div class="package__flap package__flap--top"></div>
        <div class="package__flap package__flap--bottom"></div>
        <div class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
        <div class="package__side package__side--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
          <div class="package__side package__side--flipped">
            <div class="package__flap package__flap--top"></div>
            <div class="package__flap package__flap--bottom"></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
5

The thing to remember is that whenever we add a new feature that affects the sequence, we need to update our steps. Not only the

.package {
  height: calc(var(--height, 20) * 1vmin);
  width: calc(var(--width, 20) * 1vmin);
}
5 values, but also the
.package__flap {
  width: 99.5%;
  height: 49.5%;
  background: var(--flap-bg, var(--face-4));
  position: absolute;
  left: 50%;
  transform: translate(-50%, 0);
}
.package__flap--top {
  transform-origin: 50% 100%;
  bottom: 100%;
}
.package__flap--bottom {
  top: 100%;
  transform-origin: 50% 0%;
}
.package__side--extra > .package__flap--bottom,
.package__side--tabbed > .package__flap--bottom {
  top: 99%;
}
.package__side--extra > .package__flap--top,
.package__side--tabbed > .package__flap--top {
  bottom: 99%;
}
8 value.

That’s it!

That’s how you make a pure CSS 3D package toggle. Are you going to drop this into your website? Unlikely! But, it’s fun to see how you might achieve these things with CSS. Custom properties are so powerful.