My book on iOS interface design, Design Teardowns: Step-by-step iOS interface design walkthroughs is now available!

From UIKit to Web

In this short series, we explore how to tackle some UI questions for the web with respect to how things are done on iOS.

Let's revisit a loading spinner animation we've worked on before. If you want to see how this is implemented on iOS, check it out here.

Here's what we're aiming for:

Circles

Let's start by rendering something circular on the screen. We can do this using the border-radius attribute:

HTML

<div class="circle"></div>  

CSS

.circle {
  display: inline-block;
  background: #444;
  height: 80px;
  width: 80px;
  border-radius: 40px;
}

Here's what we get:

By setting the border radius of each quarter to the half the size of the element, we've rounded the square into a circle.

This is a great start but we don't have any ability to animate its appearance. To do any useful animation at all, we should consider semi-circle and apply some clipping to it. (If you want some additional reading, check out this link.)

Semi-circles

Let's create a semi-cirle:

HTML

<div class="container">  
  <div class="wedge"></div>
</div>  

CSS

.container {
  height: 80px;
  width: 80px;
  position: relative;
  display: inline-block;
}

.wedge {
  background: #444;
  height: 80px;
  width: 40px;
  border-radius: 0 40px 40px 0;
}

Here's what we get:

Again we use the border-radius attribute, but we've specified 0 for the top-left and bottom-left.

Animating semi-circles

Next let's apply an animation:

HTML

<div class="container">  
  <div class="wedge"></div>
</div>  

CSS

.container {
  ...
}

.wedge {
  background: #444;
  height: 80px;
  width: 40px;
  border-radius: 0 40px 40px 0;
  animation: wedge-animation 1.5s infinite linear;
  transform-origin: 0 50%;
}

@keyframes wedge-animation {
  0% {
    transform: rotateZ(-180deg);
  }
  50% {
    transform: rotateZ(0deg);
  }
  100% {
    transform: rotateZ(180deg);    
  }
}

Here's what we get:

We've setup a key-frame animation with the @keyframes keyword. To produce this rotation, we start at -180 degrees initially (0%), rotate to 0 degrees halfway through (50%) and finally end at 180 degrees (100%).

Next, we add this animation to our wedge class with a duration of 1.5 seconds, with a linear curve. We also move the transform-origin to the left-most position so it seems to rotate about the center of our "circle".

Clipping the semi-circle

We'll apply a clipping mask. We do this by wrapping our semi-circle in a parent <div/>:

HTML

<div class="container">  
  <div class="masking">
    <div class="wedge"></div>
  </div>
</div>  

CSS

.container {
  ...
}

.masking {
  width: 40px;
  overflow: hidden;
}

.wedge {
  ...
}

@keyframes wedge-animation {
  ...
}

Here's what we get:

Because we've made the parent <div/> as wide as the radius of the circle, as the semi-circle rotates past the origin it gets clipped. This is the crucial bit that makes the animation work - the shape seems to grow and shrink as it gets clipped.

Full circle

Next, we'll add the other half semi-circle:

HTML

<div class="container">  
  <div class="masking pushed">
    <div class="wedge wedge1"></div>
  </div>
  <div class="masking">
    <div class="wedge wedge2"></div>
  </div>
</div>  

CSS

...

.pushed {
  left: 40px;
}

.wedge {
  background: #444;
  height: 80px;
  width: 40px;
}

.wedge1 {
  border-radius: 0 40px 40px 0;
  animation: wedge-animation 1.5s infinite linear;
  transform-origin: 0 50%;
}

.wedge2 {
  border-radius: 40px 0 0 40px;
  animation: wedge-animation 1.5s infinite linear;
  transform-origin: 100% 50%;
}

@keyframes wedge-animation {
  ...
}

Here's what we get:

So we've duplicated our masking and wedge classes and specialized the wedge animation into wedge1 and wedge2 for each half of the circle. There's also a pushed class applied to wedge1 to move it over by the radius (it is the right semi-circle.)

Animation timing

The problem now is that we're using the exact same animation of each semi-circle. We need to delay the left semi-circle so both halves animate together seamlessly.

HTML

<div class="container">  
  <div class="masking pushed">
    <div class="wedge wedge1"></div>
  </div>
  <div class="masking">
    <div class="wedge wedge2"></div>
  </div>
</div>  

CSS

...

.wedge1 {
  border-radius: 0 40px 40px 0;
  animation: wedge1-animation 3.0s infinite linear;
  transform-origin: 0 50%;
}

.wedge2 {
  border-radius: 40px 0 0 40px;
  animation: wedge2-animation 3.0s infinite linear;
  transform-origin: 100% 50%;
}

@keyframes wedge1-animation {
  0% {
    transform: rotateZ(-180deg);
  }
  25%, 50% {
    transform: rotateZ(0deg);
  }
  75%, 100% {
    transform: rotateZ(180deg);
  }
}

@keyframes wedge2-animation {
  0%, 25% {
    transform: rotateZ(-180deg);
  }
  50%, 75% {
    transform: rotateZ(0deg);
  }
  100% {
    transform: rotateZ(180deg);
  }
}

Here's what we get:

What we've done is have the left semi-circle do nothing the first quarter duration and the right half animate in. In the next quarter, the right half does nothing and the left animates in. Finally, we animate the right half out, followed by the left. We've also doubled the animation duration to account for the delays.

From sector to edge

Next, we want to remove part of the circle such that we see only a thin border around the edge. We can't apply a circular mask, however. We can fake this by positioning another slightly smaller circle over the top of this shape.

HTML

<div class="container">  
  <div class="masking pushed">
    <div class="wedge wedge1"></div>
  </div>
  <div class="masking">
    <div class="wedge wedge2"></div>
  </div>
   <div class="overlay"></div>
</div>  

CSS

...

.overlay {
  background: white;
  border-radius: 50%;
  width: 76px;
  height: 76px;
  position: absolute;
  top: 2px;
  left: 2px;
}

We've added another <div/> which is 4px smaller in size. The border-radius is set to 50% which is half of the size. Finally we've set the background-color and positioned it absolutely to center it with respect to the circle behind.

Rotating everything

Finally, we'll apply a rotation to the entire hierarchy to get the look we're after.

HTML

<div class="container container-rotate">  
  <div class="masking pushed">
    <div class="wedge wedge1"></div>
  </div>
  <div class="masking">
    <div class="wedge wedge2"></div>
  </div>
   <div class="overlay"></div>
</div>  

CSS

...

.container-rotate {
  animation: container-rotate 3.0s infinite linear;
}

@keyframes container-rotate {
  0% {
    transform: rotateZ(0deg);
  }
  100% {
    transform: rotateZ(360deg);
  }
}

That's it! I hope you've enjoyed this post. In the following posts, we'll see how we can clean up our CSS and wrap our markup into a reusable component.