Polaroid CSS and very little HTML

In the previous article, we got a beautiful slider (“carousel”) with circular rotation. Today, I’m going to create a slider that scrolls through a stack of Polaroids.

Don’t look at the code yet, first I have a lot to tell you about it. Go! To the start of our Python full stack development course.

basic settings

I will take most of the HTML and CSS for this project from the circle slider code from the previous article. The HTML markup is exactly the same:

<div class="gallery">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
  <img src="" alt="">
</div>

And below is the original CSS of the parent container .gallery. A container is a grid where images are stacked one above the other:

.gallery  {
  display: grid;
  width: 220px; /* контроль размера контейнера */
}
.gallery > img {
  grid-area: 1 / 1;
  width: 100%;
  aspect-ratio: 1;
  object-fit: cover;
  border: 10px solid #f2f2f2;
  box-shadow: 0 0 4px #0007;
}

So far, nothing complicated. For the “polaroid” effect, I use the properties border and box-shadow. Try playing with styles, you might be able to achieve more! I will proceed to the most difficult – to animation.

What is the focus?

The slider logic is based on the order in which the images are stacked. Yes, we will work with z-index. At first, all pictures will have the same z-indexequal to 2. So the last image will be at the top of the stack.

The last picture then shifts to the right until the next one appears. After z-index the picture is reduced and it is placed back in the stack. Now her z-index smaller than the other images, and it moves down the stack.

Take a look at the simplified slider demo. Hover your mouse over the image to see animation.

Do the same with the rest of the pictures. Here is an algorithm with a pseudo selector :nth-child() for different images:

  • Shifting the last picture (N). The following becomes visible (N − 1).
  • Move the next image (N − 1). Another one becomes visibleN − 2).
  • Move the next picture (N − 2). Another one becomes visibleN − 3).
  • (Continue until we see the first picture)
  • Shifting the first image (1) and see the last one again (N).

Endless slider is ready!

Understanding animation

In the previous article, I wrote only one animation and played it with different delays for each image. Here will be the same approach. To get started, let’s imagine an animation timeline first for three pictures, and then for any number of them (N).

Diagram of the three stages of animation

The animation is divided into three stages – “shift right”, “shift left” and “stop”. Delays between images are defined as follows: if the animation of the first image starts at 0s (0 s), and its duration is 6s (6 s), then the animation of the second picture starts at -2s (minus 2 s), and the third – -4s (minus 4 s).

.gallery > img:nth-child(2) { animation-delay: -2s; } /* -1 * 6с / 3 */
.gallery > img:nth-child(3) { animation-delay: -4s; } /* -2 * 6с / 3 */

You can also see that the “stop” stage takes up two-thirds of the time of the entire animation (2*100%/3), and “shift right” and “shift left” in total take a third of the time. Therefore, the duration of each displacement is 100%/6 from the time of the entire animation.

Animation keyframes look like this:

@keyframes slide {
  0%     { transform: translateX(0%); }
  16.67% { transform: translateX(120%); }
  33.34% { transform: translateX(0%); }
  100%   { transform: translateX(0%); } 
}

120% is an arbitrary number. Need more value 100%. Images should move to the right outside the borders of other images. The images must be shifted by at least 100% from their size. That’s why I chose the number 120% — to get some extra space.

Next, you need to write the values z-index. Remember to update z-indeximages after how it moves to the right.

@keyframes slide {
  0%     { transform: translateX(0%);   z-index: 2; }
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; } /* здесь обновляем порядок наложения по оси z */
  33.34% { transform: translateX(0%);   z-index: 1; }
  100%   { transform: translateX(0% );  z-index: 1; }  
}

Instead of defining one state at a point in the timeline equal to 16.67% (100%/6), we define two states almost at the same point (16.66 and 16.67%), where z-index decreases before the image is moved back to the stack.

That’s what will happen after we put it all together.

Weird, the offset works fine, but the order of the images in the stack is wrong. The animation starts correctly, the top image moves backwards… But the following images don’t follow suit. Pay attention to the second picture – it returns to the top of the stack, and then the next photo appears on top of it.

Let’s take a closer look at the changes z-index. Initially, all images z-index equals 2. This means that the order of the pictures in the stack looks like this…

Наш взгляд 👀 --> третье фото (2) | второе фото (2) | первое фото (2)

The third picture is shifting, its z-index is being updated. Now the order of the pictures is:

Наш взгляд 👀 --> второе фото (2) | первое фото (2) | третье фото (1)

We do the same with the second picture:

Наш взгляд 👀 --> первое фото (2) | третье фото (1) | второе фото (1)

…and from the first:

Наш взгляд 👀 --> третье фото (1) | второе фото (1) | первое фото (1)

Everything seems to be in order, but in fact it is not! When the first picture moves back, the third one starts a new iteration, which means that its z-index becomes equal again 2:

Наш взгляд 👀 --> третье фото (2) | второе фото (1) | первое фото (1)

In fact all images never got a value z-indexequal to 2! When the images are still (for example, during the “stop” stage), z-index equals 1. If the third photo is shifted and its z-index changes from 2 before 1, it stays on top of the stack! If all images have the same `z-index, the last one in the source sequence—in this case, the third one—is on top. Offsetting the third image results in the following:

Наш взгляд 👀 --> третье фото (1) | второе фото (1) | первое фото (1)

The third image is still on top of the stack, followed by the second image moving to the top when its animation is restarted at z-index: 2:

Наш взгляд 👀 --> второе фото (2) | третье фото (1) | первое фото (1)

When it shifts, this is what happens:

Наш взгляд 👀 --> третье фото (1) | второе фото (1) | первое фото (1)

And then the first picture appears on top:

Наш взгляд 👀 --> первое фото (2) | третье фото (1) | второе фото (1)

I’m confused. It turns out that the whole logic is wrong?

I know it’s difficult. But our logic is partly correct. You need to change the animation a little, and everything will work. The trick is to correctly reset the value z-index.

Let’s go back to the stage where the third image was on top of the stack:

Наш взгляд 👀 --> третье фото (2) | второе фото (1) | первое фото (1)

After shifting the third picture and changing it z-indexshe stays on top. We need to update z-index second picture. Before moving the third image out of the stack, let’s do z-index second image equal to 2.

In other words, z-index the second picture is reset before the end of the animation.

Diagram of animation stages with symbols for where the z-index decreases or increases

Green plus means increase z-index before 2and red minus – decrease to 1. The second photo first has z-indexequal to 2and when shifted from the stack, it decreases to 1. But before the first image moves out of the stack, let’s do z-index second image equal to 2. In this way, z-index the z-index of the first image and the z-index of the second image are the same. But the third image will still end up on top, as it appears later in the DOM. After shifting the third photo and updating it z-indexit ends up at the bottom of the stack.

This happens in two-thirds of the animation. Required appropriately update keyframes:

@keyframes slide {
  0%     { transform: translateX(0%);   z-index: 2; }
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; } /* здесь обновляем порядок наложения по оси z... */
  33.34% { transform: translateX(0%);   z-index: 1; }
  66.33% { transform: translateX(0%);   z-index: 1; }
  66.34% { transform: translateX(0%);   z-index: 2; } /* ...а также здесь */
  100%   { transform: translateX(0%);   z-index: 2; }  
}

Better, but still not at all

It will never end

Don’t worry, we won’t have to change the keyframes again, because this situation only occurs if the last photo is involved in the animation. It is possible to create a “custom” keyframe animation specifically to address the last image issue.

When the first photo is on top, the following happens:

Наш взгляд 👀 -->  первое фото (2) | третье фото (1) | второе фото (1)

After the previous change, the third image will appear on top before shifting the first one. This is only happening now, because the next image that moves after the first one is last thing the image that is above in the DOM. The rest of the photos work as they should, because first comes Nthen – N - 1after 3 changes to 2a 2 on 1… But then there is a transition from 1 to N.

Avoid this by using the following keyframes for the last image:

@keyframes slide-last {
  0%     { transform: translateX(0%);   z-index: 2;}
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; } /* здесь обновляем порядок наложения по оси z... */
  33.34% { transform: translateX(0%);   z-index: 1; }
  83.33% { transform: translateX(0%);   z-index: 1; }
  83.34% { transform: translateX(0%);   z-index: 2; } /* ...а также здесь */
  100%   { transform: translateX(0%);   z-index: 2; }
}

z-index resets to 5/6 of the animation (not 2/3) when the first photo is out of the stack. No more jumping pictures!

Hooray. Now everything works flawlessly! Here is the final code in all its glory:

.gallery > img {
  animation: slide 6s infinite;
}
.gallery > img:last-child {
  animation-name: slide-last;
}
.gallery > img:nth-child(2) { animation-delay: -2s; } 
.gallery > img:nth-child(3) { animation-delay: -4s; }

@keyframes slide {
  0% { transform: translateX(0%); z-index: 2; }
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; } 
  33.34% { transform: translateX(0%); z-index: 1; }
  66.33% { transform: translateX(0%); z-index: 1; }
  66.34% { transform: translateX(0%); z-index: 2; } 
  100% { transform: translateX(0%); z-index: 2; }
}
@keyframes slide-last {
  0% { transform: translateX(0%); z-index: 2; }
  16.66% { transform: translateX(120%); z-index: 2; }
  16.67% { transform: translateX(120%); z-index: 1; }
  33.34% { transform: translateX(0%); z-index: 1; }
  83.33% { transform: translateX(0%); z-index: 1; }
  83.34% { transform: translateX(0%); z-index: 2; } 
  100%  { transform: translateX(0%); z-index: 2; }
}

Support for any number of images

Now that the animation renders the three photos correctly, let’s rewrite the code to work with any number (N) pictures. But first, let’s optimize and remove redundant code. To do this, we share the animation:

.gallery > img {
  z-index: 2;
  animation: 
    slide 6s infinite,
    z-order 6s infinite steps(1);
}
.gallery > img:last-child {
  animation-name: slide, z-order-last;
}
.gallery > img:nth-child(2) { animation-delay: -2s; } 
.gallery > img:nth-child(3) { animation-delay: -4s; }

@keyframes slide {
  16.67% { transform: translateX(120%); }
  33.33% { transform: translateX(0%); }
}
@keyframes z-order {
  16.67%,
  33.33% { z-index: 1; }
  66.33% { z-index: 2; }
}
@keyframes z-order-last {
  16.67%,
  33.33% { z-index: 1; }
  83.33% { z-index: 2; }
}

Now there is much less code! I created a separate animation for offset and another for update z-index. Please note that when animating z-index used steps(1). I use steps(1)because I want to abruptly change the value z-indexrather than gradually, as in the shear animation.

The code is now easier to read and maintain. It became clear how to work with any number of images. Animation delays and keyframe percentages need to be updated. With delays, everything is simple – you can use the loop that I wrote in the previous article to support multiple images in a circular slider:

@for $i from 2 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {
    animation-delay: calc(#{(1 — $i)/$n}*6s);
  }
}

Moving from vanilla CSS to SASS. Then imagine how the timeline changes when there is N number of images. Do not forget that the animation consists of three stages:

Demonstration of three stages of animation using arrows.

After “shift right” and “shift left”, the image should remain still until other pictures go through animations. So the “stop” stage should last as long as the stages (N − 1) “shift to the right” and “shift to the left”. During one iteration “shift” N images. Thus, “shift right” and “shift left” together last 100%/N of the total animation time. The image comes out of the stack in (100%/N)/2 and goes back into the stack at 100%/N.

This code:

@keyframes slide {
  16.67% { transform: translateX(120%); }
  33.33% { transform: translateX(0%); }
}

replace with this one:

@keyframes slide {
  #{50/$n}%  { transform: translateX(120%); }
  #{100/$n}% { transform: translateX(0%); }
}

If replace N on 3it turns out 16.67 and 33.33%, if there are three photos in the stack. Applying the same logic for stacking order, we get the following:

@keyframes z-order {
  #{50/$n}%,
  #{100/$n}% { z-index: 1; }
  66.33% { z-index: 2; }
}

We still need to update the value 66.33%. At this point, it drops z-index image before the end of the animation and at the same time the next image starts to move. Offset takes 100%/Nand reset z-index happening in 100% — 100%/N:

@keyframes z-order {
  #{50/$n}%,
  #{100/$n}% { z-index: 1; }
  #{100 — 100/$n}% { z-index: 2; }
}

But for the animation to work z-order-last reset z-index should happen a little later. Remember the fix for the last photo? Reset z-index should happen when the first picture is out of the stack, not when it starts shifting. Here is a similar approach:

@keyframes z-order-last {
  #{50/$n}%,
  #{100/$n}% { z-index: 1; }
  #{100 — 50/$n}% { z-index: 2; }
}

Ready! Here is the slider with five pictures:

For beauty, let’s add a twist:

I just added rotate(var(--r)) to property transform. In a cycle --r – random angle:

@for $i from 1 to ($n + 1) {
  .gallery > img:nth-child(#{$i}) {
    --r: #{(-20 + random(40))*1deg}; /* случайный угол между -20 и 20 градусами */
  }
}

Turning results in small glitches. Sometimes the images jump to the end of the stack, but that doesn’t matter.

Conclusion

All work with z-index required fine tuning. If before you weren’t sure how the stacking order works, now I hope you understand how it works much better! If something seemed incomprehensible to you, I strongly advise you to read this article again and write it all down on paper. Try to illustrate each animation step using a different number of images to get a better idea of ​​how it all works.

In the last article, I showed some geometric tricks and created a looped circular slider. Today – did something similar with z-index. In both cases, I didn’t duplicate pictures to create a continuous animation and didn’t use JavaScript.

And in the next article, I will show you how to create a 3D slider. Don’t switch!

Brief catalog of courses

Data Science and Machine Learning

Python, web development

Mobile development

Java and C#

From basics to depth

As well as

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *