The KIRUPA orange logo! A stylized orange made to look like a glass of orange juice!

FORUM

Animating Items Around a Point

by kirupa   |   filed under Web Animation

A fun animation with a good dose of technical quirkiness is one where we have items rotate around a particular point. Take a look at the following:

Notice that we have a bunch of different food-related emojis spinning around a very happy smiley in the middle. In this article, we'll look at how to make this spinning work and go down some deep technical alleys to understand why it works the way it does. To make the most of our time here, beyond having a basic understanding of HTML and CSS, make sure you have brushed up on your knowledge of CSS transforms and the basics of how to create an animation in CSS. Those two topics will be front and center of what we will be looking closely at.

Onwards!

The Concepts

Before we get to the technical details, let's first take a step back and talk about how we can pull off this animation. By default, let's say we have a fixed-size element like a square (could be adiv, image, or any other element) that we want to rotate:

There is an origin point all of these elements have. This is typically the vertical and horizontal center known as the transform origin. It is from this origin point all of our element's calculations for size, rotation, position, etc. will be based on. Since we are going to be focusing on rotation quite a bit, when we rotate an element, the element will rotate around its origin point:

When the origin point is in the middle of the element, our element will look to be spinning in-place:

To rotate an element around another point altogether, we need to change this behavior. There are two things we need to do.

  1. Ensure our element is centered directly over the point we want to rotate around
  2. Shift (aka translate) our element away from the origin point to define the radius or how wide the rotation will be

The first step is the trickiest. To ensure our element is centered over the point we want to rotate around, there are too many situational things we need to think about. This could be an absolute position we specify. It could be centering and overlaying using Grid or Flexbox. There are a variety of ways to do this, so we'll have to handle this on a case-by-case basis. For now, let's just assume that we have a way of placing our element over the element we wish to rotate around.

What isn't situational is the second step where we translate our element. If we translate our square horizontally, it would look as follows:

Here is the thing to keep in mind when translating an element in CSS. When we translate an element, we aren't physically moving the element. The element is still in its original position. It just appears to have moved over:

Any transform-related changes we make, such as a rotate, will occur as if the element is still in its original position. This detail is crucial to pulling of our rotation effect, for if we rotate our square after this translation, instead of the square rotating around its point of origin, what we actually get is our desired effect:

Fast-forwarding a few steps back and letting this rotation run its full course, we can see how this would look:

The size of the rotation is determined by how far we translated our square. Pretty neat, right? There are a few more details we need to consider, but we covered the big ones here. We'll deal with these additional details as part of building our rotation effect out.

Putting it All Together

In the previous sections, we conceptually looked at how to pull off our effect where we rotate an element around a point. It's time to turn all of those images and words into something our browser understands.

Starting Point

If you want to actively follow along (as opposed to kicking back and just reading 🛋️), create a new HTML document and add the following markup to get our example started:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Example: Rotation Around a Point</title>

  <style>
    body {
      display: grid;
      justify-content: center;
      width: 100vw;
    }

    h1 {
      font-family: sans-serif;
      background: linear-gradient(180deg,
          rgba(255, 255, 255, 0) 0%,
          rgba(0, 0, 0, 0) 50%,
          rgba(255, 204, 77, .5) 50%,
          rgba(255, 204, 77, .5) 100%);
      padding: 2px;
      width: fit-content;
    }

    .main {
      width: 500px;
      height: 350px;
      background-color: #F2F7F2;
      border-color: #EEE;
      border-radius: 10px;
      border-width: 2px;

      display: grid;
      justify-content: center;
      align-content: center;
    }

    .main > * {
      grid-column: 1;
      grid-row: 1;
    }

    .theSmiley {
      align-self: center;
      justify-self: center;
      width: 50px;
      height: 50px;
    }

    .orange img {
      width: 50px;
      display: block;
      margin: 0 auto;
      margin-top: 50px;
    }
  </style>
</head>

<body>
  <h1>Rotation Around a Point</h1>
  <div class="main">
    <img class="theSmiley" src="https://www.kirupa.com/icon/1f600.svg">
  </div>
  <a class="orange" href="https://www.kirupa.com">
    <img src="https://www.kirupa.com/images/orange.png">
  </a>
</body>

</html>

After you've added all of this, save your document and preview it in your browser to make sure everything looks good. What you see should look something like the following:

Once you have verified that your starting point matches mine, take a few moments to understand what the HTML and CSS are doing. The main action happens in the appropriately named main div element that houses the smiley that we want our items to rotate around:

<div class="main">
  <img class="theSmiley" src="https://www.kirupa.com/icon/1f600.svg">
</div>

The main div is a rectangular container that will house our images, like the smiley and our food items. The contents of this container will be centered horizontally and vertically, as specified by the following grid-related properties:

.main {
  width: 500px;
  height: 350px;
  background-color: #F2F7F2;
  border-color: #EEE;
  border-radius: 10px;
  border-width: 2px;

  display: grid;
  justify-content: center;
  align-content: center;
}

The contents will also stack on top of each other, and the .main > * style rule is responsible for that behavior:

.main > * {
  grid-column: 1;
  grid-row: 1;
}

These two style rules combined are what help our smiley to be located perfectly at the center of of the main div container. As we will see very shortly, this is what will also allow our subsequent food-related images to be centered perfectly as well.

Adding an Item to Rotate

With our starting point all ready to go, our first real task is to add an item to rotate. To do this, add the following highlighted line to our HTML:

<div class="main">
  <img class="theSmiley" src="https://www.kirupa.com/icon/1f600.svg">
  <img class="item" src="https://www.kirupa.com/icon/1f951.svg">
</div>

We added an image element with a class value of food to our main divcontainer. If you save your changes and check the results in your browser, you will see our newly added image (of an avocado!) centered and placed directly above our smiley:

Since we want our food images to rotate around our smiley, we also finished our first step of positioning our rotated element directly over the point we wish to rotate around. This was automatically handled for us because we already had specified the various grid-related properties to ensure horizontal and vertical centering and stacking.

Translating and Rotating the Item

Our next step is to translate and rotate our item. As strange as this will sound, both of these transformations are done as part of the animation keyframes. Go ahead and add the following blocks of CSS towards the bottom of your style block (just above the closing </style> tag):

.item {
  animation: spinAround 6s linear infinite;
}

@keyframes spinAround {
  from {
    transform: rotate(0deg) translate(120px);
  }
  to {
    transform: rotate(360deg) translate(120px);
  }
}

Before you do anything else, save these changes and preview in the browser. If everything worked, you'll see that our avocado properly rotates around the smiley:

Looking more closely at the CSS, we have our .item style rule that targets our avocado image element. This rule contains the animation property that specifies details about the, um...animation:

.item {
  animation: spinAround 6s linear infinite;
}

The relevant details are that our animation will take 6 seconds to run, will loop forever, and run linearly with no easing (aka acceleration). The exact behavior of the animation is defined by the spinAround keyframes:

@keyframes spinAround {
  from {
    transform: rotate(0deg) translate(120px);
  }
  to {
    transform: rotate(360deg) translate(120px);
  }
}

It is inside these keyframes the entirety of our rotating and translating business lives. In our from and to keyframes, we specify the rotation from 0deg to 360deg respectively. We also specify that the image needs to be translated by 120px, and this detail is duplicated across our from and to keyframes as well.

Why specify our translate inside the keyframes?

Now, that our rotation from 0 to 360 degrees is specified as part of our animation keyframes makes sense. That is the property that will actually be animated. Why would we specify the 120px translate in the keyframes as well? Wouldn't it make more sense to have the translate be defined just once inside our .item style rule instead? This strangeness has to do with a CSS quirk where only one transform declaration will win out in the end. We can't specify individual transform functions (at least not yet) and have them work independently. If we specify the translate transform in the .food rule, the rotate transform in our animation keyframes will override the translate transform since it will get applied last. The only way to work around this is to have the final transform declaration that gets applied contain all of the transform functions we'll want to set as well.

Adding Additional Items

Now that we have one food item revolving around our smiley, it's time to add more! Go back to your HTML and add the following highlighted image elements:

<div class="main">
  <img class="theSmiley" src="https://www.kirupa.com/icon/1f600.svg">
  <img class="item" src="https://www.kirupa.com/icon/1f951.svg">
  <img class="item" src="https://www.kirupa.com/icon/1f370.svg">
  <img class="item" src="https://www.kirupa.com/icon/1f355.svg">
  <img class="item" src="https://www.kirupa.com/icon/1f96c.svg">
  <img class="item" src="https://www.kirupa.com/icon/1f347.svg">
  <img class="item" src="https://www.kirupa.com/icon/1f354.svg">
</div>

If we save these changes and preview what happens in the browser, what we'll see will look something a bit like the following:

All of the images will overlap each other and rotate in unison. The rotating in unison part we totally want. That they overlap over each other is something we'll want to fix, and we'll fix this is by setting an offset value for the animation-delay property on each rotating image. The way we calculate the amount to offset the animation on each image by is by dividing the animation duration by the number of images:

Once we have the offset value, we set that to the animation-delay property by putting a negative sign up front and adding an s to signify that it is a seconds time unit.

For our example, the animation has a duration of 6 seconds. We have 6 images we wish to rotate. This makes the math for our animation-delay offset easy where 6 divided 6 is 1. The negative of that is -1, so we start our offset by -1s and increment by that amount for each additional image. All of this applied to our images will look as follows, so make the highlighted addition of the inline style attribute and animation-delay value:

<div class="main">
  <img class="theSmiley" src="https://www.kirupa.com/icon/1f600.svg">
  <img class="item" style="animation-delay: -1s;" src="https://www.kirupa.com/icon/1f951.svg">
  <img class="item" style="animation-delay: -2s;" src="https://www.kirupa.com/icon/1f370.svg">
  <img class="item" style="animation-delay: -3s;" src="https://www.kirupa.com/icon/1f355.svg">
  <img class="item" style="animation-delay: -4s;" src="https://www.kirupa.com/icon/1f96c.svg">
  <img class="item" style="animation-delay: -5s;" src="https://www.kirupa.com/icon/1f347.svg">
  <img class="item" style="animation-delay: -6s;" src="https://www.kirupa.com/icon/1f354.svg">
</div>

The result of this will look a little bit better when previewed in our browser:

All of our food items are now evenly spaced! There is one last change to make. The rotating items look a bit too large. It would be better to have them be sized smaller relative to our smiley they are rotating, so let's fix that by going back to our keyframes and adding a scale function to our existing transform declarations:

@keyframes spinAround {
  from {
    transform: rotate(0deg) translate(120px) scale(.7);
  }
  to {
    transform: rotate(360deg) translate(120px) scale(.7);
  }
}

This will shrink our food items down into something more bite sized for our smiley to handle! And with this change, we have our rotating effect built-out to look exactly like what we started off this article with.

Going a Bit Further

Before we wrap things up, let's create one more (slightly more involved) example using what we've learned. The original posting date for this article was around Christmas, so it seems appropriate to have something more holiday-ey like the following:

Notice there are a few changes from the food-related animation we saw and built earlier. There are more items rotating around our Santa smiley. Each individual item is rotating around its own point of origin in addition to rotating around our Santa smiley. It's like a a double rotation!

Instead of building this example together, I'm going to go ahead and provide you the full markup needed to create this example. We'll then walk through the big changes and why they are represented the way they are. The full HTML and CSS for this example is:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Rotation Around a Point</title>

  <style>
    body {
      display: grid;
      justify-content: center;
      width: 100vw;
    }

    h1 {
      font-family: sans-serif;
      background: linear-gradient(180deg,
          rgba(255, 255, 255, 0) 0%,
          rgba(0, 0, 0, 0) 50%,
          rgba(255, 204, 77, .5) 50%,
          rgba(255, 204, 77, .5) 100%);
      padding: 2px;
      width: fit-content;
    }

    .main {
      width: 500px;
      height: 350px;
      background-color: #1e2231;
      border-color: #EEE;
      border-radius: 10px;
      border-width: 2px;

      display: grid;
      justify-content: center;
      align-content: center;
    }

    .main > * {
      grid-column: 1;
      grid-row: 1;
    }

    .theSmiley {
      width: 50px;
      height: 50px;
    }

    .orange img {
      width: 50px;
      display: block;
      margin: 0 auto;
      margin-top: 50px;
    }

    .item {
      animation: spinAround 15s linear infinite;
    }

    .main > * > img {
      animation: rotate 5s linear infinite;
    }

    @keyframes rotate {
      from {
        transform: rotate(0deg);
      }
      to {
        transform: rotate(360deg);
      }
    }

    @keyframes spinAround {
      from {
        transform: rotate(0deg) translate(140px) scale(.7);
      }
      to {
        transform: rotate(360deg) translate(140px) scale(.7);
      }
    }
  </style>
</head>

<body>
  <h1>Rotation Around a Point</h1>
  <div class="main">
    <img class="theSmiley" src="https://www.kirupa.com/icon/1f385.svg">
    <div class="item" style="animation-delay: -1.5s;">
      <img src="https://www.kirupa.com/icon/1f9e6.svg">
    </div>
    <div class="item" style="animation-delay: -3s;">
      <img src="https://www.kirupa.com/icon/1f36a.svg">
    </div>
    <div class="item" style="animation-delay: -4.5s;">
      <img src="https://www.kirupa.com/icon/1f95b.svg">
    </div>
    <div class="item" style="animation-delay: -6s;">
      <img src="https://www.kirupa.com/icon/2603.svg">
    </div>
    <div class="item" style="animation-delay: -7.5s;">
      <img src="https://www.kirupa.com/icon/1f381.svg">
    </div>
    <div class="item" style="animation-delay: -9s;">
      <img src="https://www.kirupa.com/icon/1f98c.svg">
    </div>
    <div class="item" style="animation-delay: -10.5s;">
      <img src="https://www.kirupa.com/icon/2744.svg">
    </div>
    <div class="item" style="animation-delay: -12s;">
      <img src="https://www.kirupa.com/icon/1f31f.svg">
    </div>
    <div class="item" style="animation-delay: -13.5s;">
      <img src="https://www.kirupa.com/icon/1f525.svg">
    </div>
    <div class="item" style="animation-delay: -15s;">
      <img src="https://www.kirupa.com/icon/1f384.svg">
    </div>
  </div>
  <a class="orange" href="https://www.kirupa.com">
    <img src="https://www.kirupa.com/images/orange.png">
  </a>
</body>

</html>

If you copy / paste all of this into your own HTML document and preview in your browser, you will see my example working in your own browser. Now, let's dive into the relevant new additions.

Adding an Extra Per-Item Rotation

Each of our Christmas-themed items not only rotate around the Santa smiley, they each rotate around their own point of origin as well. The way we pull this off is by wrapping each rotating image inside its own div element:

<div class="main">
  <img class="theSmiley" src="https://www.kirupa.com/icon/1f385.svg">
  <div class="item" style="animation-delay: -1.5s;">
    <img src="https://www.kirupa.com/icon/1f9e6.svg">
  </div>
  <div class="item" style="animation-delay: -3s;">
    <img src="https://www.kirupa.com/icon/1f36a.svg">
  </div>
  <div class="item" style="animation-delay: -4.5s;">
    <img src="https://www.kirupa.com/icon/1f95b.svg">
  </div>
  <div class="item" style="animation-delay: -6s;">
    <img src="https://www.kirupa.com/icon/2603.svg">
  </div>
  <div class="item" style="animation-delay: -7.5s;">
    <img src="https://www.kirupa.com/icon/1f381.svg">
  </div>
  <div class="item" style="animation-delay: -9s;">
    <img src="https://www.kirupa.com/icon/1f98c.svg">
  </div>
  <div class="item" style="animation-delay: -10.5s;">
    <img src="https://www.kirupa.com/icon/2744.svg">
  </div>
  <div class="item" style="animation-delay: -12s;">
    <img src="https://www.kirupa.com/icon/1f31f.svg">
  </div>
  <div class="item" style="animation-delay: -13.5s;">
    <img src="https://www.kirupa.com/icon/1f525.svg">
  </div>
  <div class="item" style="animation-delay: -15s;">
    <img src="https://www.kirupa.com/icon/1f384.svg">
  </div>
</div>

We also add our item class and animation-delay inline style to this div as well. What we are doing is making this wrapper div be the item that rotates around our smiley. Because the image we are rotating is a child of this div, it happens to go along for the ride. To rotate the image itself, we have these additional style rules that target the image directly:

.main .item img {
  animation: rotate 5s linear infinite;
}

@keyframes rotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

This is almost a replica of the styles we have for rotating our div element. The difference is that we are only specifying the rotate transform inside our keyframes. There is no translation needed because we are only rotating our images around the origin point. The reason why we have to wrap our images separately from our div element and animate them individually has to, again, do with the fact that our transforms can't be independently set and combined. Applying different transforms to each parent/child element, like we have done is one awkward way to work around this limitation.

Revisiting the Animation-Delay Value

The last change I want to cover is the inline animation-delay style property we have set on each div element. To reiterate what we saw earlier, the way to calculate the offset value is to divide the animation duration by the number of items we are animating. In the case of our animation right now, our animation runs each iteration in 15 seconds, and we have 10 items we are animating. This means our offset value is 15 divided by 10 or 1.5. That is why the animation-delay for each of our items starts off at -1.5s, -3s, -4.5s, and so on until we hit the final offset value of -15. And...we are now done!

Conclusion

On the surface, rotating items around a point may seem pretty straightforward. It isn't until we start to build it out that we realize there are a bunch of quirks and hidden details that surface. When I was thinking about how to create this effect, the part I had the most difficulty with was spacing the items evenly around the point they are rotating around. Once I figured out the relationship betweeen duration and number of items as a way of calculating the offset, things snapped into place. That is what I find most exciting about creating little examples like this. Seemingly unrelated details end up playing a crucial role in unexpected ways.

Got a question or just want to chat? Comment below or drop by our forums (they are actually the same thing!) where a bunch of the friendliest people you'll ever run into will be happy to help you out!

When Kirupa isn’t busy writing about himself in 3rd person, he is practicing social distancing…even on his Twitter, Facebook, and LinkedIn profiles.

Hit Subscribe to get cool tips, tricks, selfies, and more personally hand-delivered to your inbox.

COMMENTS

Serving you freshly baked content since 1998!
Killer hosting by (mt) mediatemple

Twitter Youtube Facebook Pinterest Instagram Github