Home > Web Front-end > CSS Tutorial > How to Make a Pure CSS 3D Package Toggle

How to Make a Pure CSS 3D Package Toggle

尊渡假赌尊渡假赌尊渡假赌
Release: 2025-03-15 09:35:09
Original
375 people have browsed it

How to Make a Pure CSS 3D Package Toggle

Do you know those completely flat corrugated boxes? You can fold them and tape them to make a practical box. When recycle is needed, cut them open and flatten them. Recently, I was consulted with a 3D animation version of this concept and I thought it would be an interesting tutorial to implement it with pure CSS, so we got started!

What would this kind of animation look like? How do we create a wrapping timeline? Can the size be adjusted flexibly? Let's create a pure CSS package toggle effect.

The final effect is as follows: click to pack and unpack the carton.

Where to start?

Where to start? It is best to make plans in advance. We know we need a wrapping template and we need to fold it in three-dimensional space. If you are not familiar with CSS 3D, I suggest you read this article to get started.

If you are familiar with 3D CSS, you may tend to build a cuboid and then start. However, this brings some problems. We need to consider how the package is converted from 2D to 3D.

Let's start by creating a template. We need to plan the markings in advance and think about how we want the packaging animation to work. Let's start with some HTML.

<div>
  <div>
    <div>
      <div>
        <div></div>
        <div></div>
        <div>
          <div></div>
          <div></div>
        </div>
        <div>
          <div></div>
          <div></div>
          <div>
            <div></div>
            <div></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
Copy after login

Mixin is a good idea

There is a lot of content here. There are many divs. I often like to use Pugs to generate tags so that the content can be divided into reusable chunks. For example, each side has two covers. We can create a Pug mixin for the side and apply modifier class name using properties to make all these tags easier to write.

 mixin flaps()
  .package__flap.package__flap--top
  .package__flap.package__flap--bottom

mixin side(class)
  .package__side(class=`package__side--${class || 'side'}`)
     Flaps()
    Block

.scene
  .package__wrapper
    .package
       side('main')
         side('extra')
         side('flipped')
           side('tabbed')
Copy after login

We used two mixins. One creates a cover for each side of the box. Another creates the side of the box. Note that in the side mixin we use block. The child elements of the mixin usage are presented here, which is especially useful because we need to nest some sides later to simplify our work.

Generated tags:

<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--extra">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
      </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 class="package__side package__side--tabbed">
          <div class="package__flap package__flap--top"></div>
          <div class="package__flap package__flap--bottom"></div>
        </div>
      </div>
    </div>
  </div>
</div>
Copy after login

Nested side

Nested sides make folding wrap easier. Just like there are two covers on each side. The side child elements can inherit the side transformation and then apply their own transformation. If we start with a cuboid, it is difficult to take advantage of this.

Check out this demo, which switches between nested and non-nested elements to see the actual effect.

Each box has a transform-origin set to the bottom right corner with a value of 100% 100%. Selecting the "Transform" toggle will rotate each box by 90 degrees. However, note that if we nest elements, the behavior of that transformation changes.

We switched the tags for both versions, but nothing else was changed.

Nesting:

<div>
  <div>
    <div>
      <div>
        <div></div>
      </div>
    </div>
  </div>
</div>
Copy after login

Non-Nested:

<div>
  <div></div>
  <div></div>
  <div></div>
  <div></div>
</div>
Copy after login

Transform everything

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

The style specifies different colors and locates the sides to the package. Each side is positioned relative to the "main" side. (You will soon understand why nesting is so useful.)

Some things need to be paid attention to. Just like using a cuboid, we use --height , --width and --depth variables to determine the size. This will make it easier for us to change the package size later on.

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

Why define the size in this way? We used a unitless default size of 20, an idea I got from Lea Verou's 2016 CSS ConfAsia talk (starting at 52:44). Use custom properties as "data" instead of "value", we can use them as we like using calc() . Also, JavaScript doesn't have to care about the unit of value, we can change it to pixels, percentages, etc. without changing it elsewhere. You can refactor it to coefficients in --root , but this can quickly become overly complex.

The cover plates on each side also need to be slightly smaller than the sides to which they belong. This way, when we fold them, there is no z-index conflict.

 .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%;
}
Copy after login

We also began to consider transform-origin for each component. The top cover will rotate from its bottom edge and the bottom cover will rotate from its top edge.

We can use pseudo-elements to create the label on the right. We use clip-path to get the 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%;
}
Copy after login

Let's start using our templates on a 3D plane. We can first rotate .scene on the X-axis and Y-axis.

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

fold

We are ready to start folding our templates! Our templates will be collapsed according to custom properties --packaged . If the value is 1, the template can be collapsed. For example, let's collapse some side and pseudo-element labels.

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

Alternatively, we can write a rule for all sides that are not the "main" side.

 .package__side:not(.package__side--main),
.package__side:not(.package__side--main):after {
  transform: rotateY(calc((var(--packaged, 0) * var(--rotation, 90)) * 1deg));
}
.package__side--tabbed { --rotation: -90; }
Copy after login

This will cover all sides.

Remember when I said that nested sides allow us to inherit the transformation of the parent element? If we update our demo so that we can change the value of --packaged , we can see how that value affects the transformation. Try sliding the value of --packaged between 1 and 0 and you'll see what I mean.

Now that we have a way to switch the folded state of the template, we can start to deal with some movement. Our previous demonstration switches between two states. We can use transition for this. The fastest way? Add a transition transform of each child element in .scene .

 .scene *,
.scene *::after {
  transition: transform calc(var(--speed, 0.2) * 1s);
}
Copy after login

Multi-step transition!

But we don't fold the entire template at once - in real life, the folding process is in sequence, we will fold one side and its cover first, then move on to the next one, and so on. Scope custom properties are ideal for this purpose.

 .scene *,
.scene *::after {
  transition: transform calc(var(--speed, 0.2) * 1s) calc((var(--step, 1) * var(--delay, 0.2)) * 1s);
}
Copy after login

Here we say that for each transformation, use transition-delay to multiply --step by --delay . --delay value does not change, but each element can define its "steps" in the sequence. We can then clearly state the order in which things happen.

 .package__side--extra {
  --step: 1;
}
.package__side--tabbed {
  --step: 2;
}
.package__side--flipped,
.package__side--tabbed::after {
  --step: 3;
}
Copy after login

Check out the demo below to better understand how it works. Change the slider value to update the order in which things happen. Can you change which car wins?

The same technology is the key to what we need to do next. We could even introduce a --initial-delay , adding a slight pause to everything for a more realistic effect.

 .race__light--animated,
.race__light--animated:after,
.car {
  animation-delay: calc((var(--step, 0) * var(--delay-step, 0)) * 1s);
}
Copy after login

If we look back at our package, we can go a step further and apply the "steps" to all the elements that will be converted. This is quite verbose, but it does work. Alternatively, you can inline these values ​​in the tag.

 .package__side--extra > .package__flap--bottom {
  --step: 4;
}
.package__side--tabbed > .package__flap--bottom {
  --step: 5;
}
.package__side--main > .package__flap--bottom {
  --step: 6;
}
.package__side--flipped > .package__flap--bottom {
  --step: 7;
}
.package__side--extra > .package__flap--top {
  --step: 8;
}
.package__side--tabbed > .package__flap--top {
  --step: 9;
}
.package__side--main > .package__flap--top {
  --step: 10;
}
.package__side--flipped > .package__flap--top {
  --step: 11;
}
Copy after login

However, it feels unrealistic.

Maybe we should flip the box

If I fold the box in real life, I might flip the box over first and then fold the top cover. How can we do this? Well, those with sharp eyes may have noticed the .package__wrapper element. We will use it to slide the package. Then we rotate the wrap around the x-axis. This will give the impression of turning the package to the side.

 .package {
  transform-origin: 50% 100%;
  transform: rotateX(calc(var(--packaged, 0) * -90deg));
}
.package__wrapper {
  transform: translate(0, calc(var(--packaged, 0) * -100%));
}
Copy after login

Adjust the --step declaration accordingly and we can get an effect similar to this.

Expand the box

If you switch between folded and unfolded states, you will notice that the expansion looks wrong. The expansion order should be the complete reverse of the collapse order. We can flip --step according to --packaged and the number of steps. Our latest step is 15. We can update our conversion to this:

 .scene *,
.scene *:after {
  --no-of-steps: 15;
  --step-delay: calc(var(--step, 1) - ((1 - var(--packaged, 0)) * (var(--step) - ((var(--no-of-steps) 1) - var(--step)))));
  transition: transform calc(var(--speed, 0.2) * 1s) calc((var(--step-delay) * var(--delay, 0.2)) * 1s);
}
Copy after login

This is indeed a lot of calc to reverse transition-delay . But, it does work! We must remind ourselves to keep --no-of-steps value up to date!

We have another option. As we continue on the "Pure CSS" route, we will eventually use the checkbox trick to switch the folded state. We can have two sets of defined "steps", one of which is active when our checkbox is selected. This is certainly a more verbose solution. However, it does allow us to control more granularly.

 /* Fold*/
:checked ~ .scene .package__side--extra {
  --step: 1;
}
/* Expand*/
.package__side--extra {
  --step: 15;
}
Copy after login

Size and center

Before we give up on dat.gui in the demo, let's adjust the size of the package. We want to check if our package remains centered when folded and flipped. In this demo, the wrap has a larger --height , while .scene has a dotted border.

We might as well adjust our transformation to better center the package:

 /* Consider the package height by translation on the z-axis*/
.scene {
  transform: rotateX(calc(var(--rotate-x, -24) * 1deg)) rotateY(calc(var(--rotate-y, -32) * 1deg)) rotateX(90deg) translate3d(0, 0, calc(var(--height, 20) * -0.5vmin));
}
/* Consider the wrap depth by sliding the depth before flipping*/
.package__wrapper {
  transform: translate(0, calc((var(--packaged, 0) * var(--depth, 20)) * -1vmin));
}
Copy after login

This gives us a reliable centering effect in the scene. But it all depends on personal preference!

Tips for adding checkboxes

Now let's get rid of dat.gui and make it "pure" CSS. To do this, we need to introduce a bunch of controls into HTML. We will use a checkbox to collapse and expand our parcel. We will then use a radio button to select the package size.

 <label for="one">S</label>

<label for="two">M</label>

<label for="three">L</label>

<label for="four">XL</label>

<input type="checkbox" id="package">
<label for="package" class="open">Open the package</label>
<label for="package" class="close">Close the package</label>
Copy after login

In the final demo, we will hide the input and use the tag element. But, now, let's make them all visible first. The trick is to use the sibling combiner (~) when some controls are selected. We can then set the custom property value on .scene .

 #package:checked ~ .scene {
  --packaged: 1;
}
#one:checked ~ .scene {
  --height: 10;
  --width: 20;
  --depth: 20;
}
#two:checked ~ .scene {
  --height: 20;
  --width: 20;
  --depth: 20;
}
#three:checked ~ .scene {
  --height: 20;
  --width: 30;
  --depth: 20;
}
#four:checked ~ .scene {
  --height: 30;
  --width: 20;
  --depth: 30;
}
Copy after login

Here is a demo with this feature!

Final polish

Now we can make things look "pretty" and add some extra details. Let's hide all inputs first.

 input {
  position: fixed;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}
Copy after login

We can set the size option to the circular button:

 .size-label {
  position: fixed;
  top: var(--top);
  right: 1rem;
  z-index: 3;
  font-family: sans-serif;
  font-weight: bold;
  color: #262626;
  height: 44px;
  width: 44px;
  display: grid;
  place-items: center;
  background: #fcfcfc;
  border-radius: 50%;
  cursor: pointer;
  border: 4px solid #8bb1b1;
  transform: translate(0, calc(var(--y, 0) * 1%)) scale(var(--scale, 1));
  transition: transform 0.1s;
}
.size-label:hover {
  --y: -5;
}
.size-label:active {
  --y: 2;
  --scale: 0.9;
}
Copy after login

We want to be able to click anywhere to switch the folding and expanding of the package. Therefore, our .open and .close tags will occupy the entire screen. Want to know why we have two labels? This is a little trick. If we use transition-delay and zoom in on the corresponding tag, we can hide two tags when the package is converted. This is how we fight against spam clicks (although it won't stop users from pressing the spacebar on the keyboard).

 .close,
.open {
  position: fixed;
  height: 100vh;
  width: 100vw;
  z-index: 2;
  transform: scale(var(--scale, 1)) translate3d(0, 0, 50vmin);
  transition: transform 0s var(--reveal-delay, calc(((var(--no-of-steps, 15) 1) * var(--delay, 0.2)) * 1s));
}

#package:checked ~ .close,
.open {
  --scale: 0;
  --reveal-delay: 0s;
}
#package:checked ~ .open {
  --scale: 1;
  --reveal-delay: calc(((var(--no-of-steps, 15) 1) * var(--delay, 0.2)) * 1s);
}
Copy after login

Check out this demo to see where we added background-color in .open and .close . During the conversion, neither label is visible.

We already have full functionality! However, our package is a bit bland at the moment. Let's add extra details to make it look more like a "box" such as wrapping tape and packaging labels.

Details like this are limited only by our imagination! We can use our --packaged custom property to influence anything. For example, .package__tape is converting the scaleY transformation:

 .package__tape {
  transform: translate3d(-50%, var(--offset-y), -2px) scaleX(var(--packaged, 0));
}
Copy after login

It should be noted that whenever we add new features that affect the sequence, we need to update our steps. Not only --step value, but also --no-of-steps value.

that's all!

This is how to create a pure CSS 3D parcel switching. Would you put it on your website? Not likely! However, it's fun to see that you can implement these things with CSS. Custom properties are very powerful.

Why not celebrate and give out a gift for CSS!

Stay good! ʕ •ᴥ•ʔ

The above is the detailed content of How to Make a Pure CSS 3D Package Toggle. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template