Mesmerizing animation using only CSS translations 🪄
Tags: css, html, webdev, watercooler
Published: 2021-12-02 11:11:14

Two weeks ago I shared [this animation](https://dev.to/mustapha/mesmerizing-animation-using-only-css-rotations-229l) using only rotations. This time I'm back at it, but only using translations.

Newton said time is absolute, Einstein said time is relative. I don't know who's right and who's wrong, but what I can say is that timing is everything (at least for this animation).
So what do you see?



{% codepen https://codepen.io/TheAngularGuy/pen/RwZBLbZ default-tab=result %}

ℹ️ If you can't see the Codepen above, [click here](https://twitter.com/TheAngularGuy/status/1460302224287051787) to see the animation. 

| Disclamer                                                                                                                                                         |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| On i̶n̶t̶e̶r̶n̶e̶t̶ ̶e̶x̶p̶l̶o̶r̶e̶r̶ iOS safari the implementation below doesn't work correctly because for some reason we can't animate `left` from `0%` to `100%`. So I did a special implementation for safari. I talk more about it at the end of the article. |

---

Follow me on Twitter at [twitter.com/theAngularGuy](https://twitter.com/TheAngularGuy) as I tweet (almost) every day about the things I learned in my 6+ years journey in web development ✅

---

## Step 1
Ok so let's start right away. The first thing is to place all the 12 dots on a horizontal container like this (I outlined each line wrapper):


![STEP 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0nlne7mejfhncfe1uo6h.png)

To achieve this, nothing extraordinary, we have a container `div` that contains the horizontal wrappers around the dots.

```scss
.wrapper {
  width: 200px;
  margin: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  
  .line {
    width: 100%;
    height: 20px;
    position: relative;

     .dot {
      width: 20px;
      height: 20px;
      border-radius: 50%;
      position: absolute;
      background: #fff;
    }
  }
}
```

## Step 2


![Pendulum](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kjh3f2f2xa4p53x513gx.png)

Next, we need to simulate this pendulum effect using only translations. For this we need each ball moving left and right and we also need each ball to be at a slightly lower vertical position when it hits its equilibrium position (when it's in the middle of the line wrapper).

The balls' position starts at the left, so we want them to move to the right then return to the left. So, at the start of the animation, we set `left` to `0`, at fifty percent of the animation we set it at `100%` (minus the width of the ball so it doesn't overflow), and finally, at one hundred percent of the animation we set `left` back to `0`;

With this, each ball will hit its equilibrium position at 25% (of the animation) and at 75% (on the return). So at these timings, we need to move the ball to the bottom.
So the animation code looks like this:
```scss
@keyframes move {
  0% {
    top: 0; left: 0;
  } 25% {
    top: 7px;
  } 50% {
    top: 0; left: calc(100% - 20px);
  } 75% {
    top: 7px;
  } 100% {
    top: 0; left: 0;
  }
}
```
And this is the result we have so far:

![almost there](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kx0z06sx0ms1h2zmluva.gif)


## Step 3
We are almost there. We only need two things:

1. Decrease the width of each line wrapper from bottom to top (so each line width increase from top to bottom)
2. Decrease the animation speed (or increase the animation duration) from top to bottom (because the more a ball is near the bottom the more distance it needs to travel)

You can do this in many different ways, I choose to do it using sass `for through` loop (there is also the `for to` loop but the final number is excluded).

```scss
@for $i from 1 through 12 {
  .line:nth-child(#{$i}) {
    width: calc(100% - 12% + #{$i}%);
      
    .dot {
      animation-duration: calc(1s + calc(#{$i} * 5ms));
    }
  }
}
```

And that's it actually. With only these few lines of CSS (or scss 😉) we completed this crazy animation. Here's the final code:

```scss
.wrapper {
  width: 200px;
  margin: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  
  .line {
    height: 20px;
    position: relative;
    margin-bottom: 7px;
    
    .dot {
      background: #fff;
      width: 20px;
      height: 20px;
      border-radius: 50%;
      position: absolute;
      animation: move 1s ease-in-out infinite;
    }
  }
  
  @for $i from 1 through 12 {
    .line:nth-child(#{$i}) {
      width: calc(100% - 12% + #{$i}%);
      
      .dot {
        animation-duration: calc(1s + calc(#{$i} * 5ms));
      }
    }
  }
}

@keyframes move {
  0% {
    top: 0; left: 0;
  } 25% {
    top: 7px;
  } 50% {
    top: 0; left: calc(100% - 20px);
  } 75% {
    top: 7px;
  } 100% {
    top: 0; left: 0;
  }
}
```



## Safari implementation
So like I said before, on Safari (iOS) we can't animate `left` from `0%` to `100%` (at the moment I write these lines). So we have to be a little cleaver in the way we place the dots to translate them using the `transform` property.

This fix was suggested by [u/mountainunicycler](https://www.reddit.com/user/mountainunicycler/).

There are two problems if we want to use the `transform` CSS property:
1. First, each line wrapper width is different so we can't use pixels (px unit). We have to use percent (%). But when we transform an HTML element the percent unit is relative to the element itself and not the parent container like with `left`. So if a dot is `20px` large and we translate it by `100%` it will move by `20px`. That's not what we want.
2. Second, we have two animations: from left to right and from top to bottom, and it's tricky because the two animations use the same property: `transform` (So they will overload each other).

To fix these problems, we will set the dots widths to `100%` (so when we translate them by a percentage, they will move  (pretty much) relatively to their parents since they have the same width). And to draw the dots we will use the `::after` pseudo-element and make it `20px` large and `20px` tall (original size of the dots).

Then to fix the second problem we will animate two elements instead of one (so the horizontal translation won't mess with the vertical translation since these translations will be applied to two different HTML elements). We will animate the `.dot` horizontally and we will animate the `.dot::after` pseudo-element vertically. 
So, this is the code we end up with:

```scss
// Little hack to detect safari browsers
@media not all and (min-resolution: 0.001dpcm) {
  .wrapper {
    .line {
      position: relative;
      width: 100%;
      
      .dot {
        background: transparent; // resetting the background
        box-shadow: none; // resetting the box-shadow
        width: 100%;
        animation: moveHoriz 1s ease-in-out infinite;

        &::after {
          content:'';
          position: absolute;
          background: $color;
          box-shadow: 1px 1px 12px 2px $color;
          width: 20px;
          height: 20px;
          border-radius: 50%;
          animation: moveVert ease-in-out infinite;
        }
      }
    }

    @for $i from 1 through 12 {
      .line:nth-child(#{$i}) {
        width: calc(100% - 12% + #{$i}%);
        
        .dot, .dot::after {
          animation-duration: calc(1s + calc(#{$i} * 5ms));
        }
      }
    }
  }

  @keyframes moveVert {
    0% {
      transform: translateY(0);
    } 25% {
      transform: translateY(7px);
    } 50% {
      transform: translateY(0px);
    } 75% {
      transform: translateY(7px);
    } 100% {
      transform: translateY(0);
    }
  }

  @keyframes moveHoriz {
    0% {
      transform: translateX(0);
    } 50% {
      transform: translateX(calc(100% - 20px));
    } 100% {
      transform: translateX(0);
    }
  }
}
```

ℹ️ I purposefully left the two animations because I think the first one is easier to implement and easier to grasp. But the second one (for Safari) works on all evergreen browsers and at some extent is more performant (animating the `transform` property is more performant than the `left` and `top` properties).











<br>
That's it for this post. I hope you liked it. If you did, please share it with your friends & colleagues and follow me on Twitter at [@theAngularGuy](https://twitter.com/TheAngularGuy) where I tweet about web development and computer science.

Cheers!

---

### What to read next?

{% link https://dev.to/mustapha/5-tips-to-finish-your-side-projects-and-make-money-doing-so-27eo %}