Simplifying the Apple Watch Breathe App Animation With CSS Variables
When I saw the original article on how to recreate this animation, my first thought was that it could all be simplified with the use of preprocessors and especialy CSS variables. So let’s dive into it and see how!
The structure
We keep the exact same structure.
In order to avoid writing the same thing multiple times, I chose to use a preprocessor.
My choice of preprocessor always depends on what I want to do, as, in a lot of cases, something like Pug offers more flexibility, but other times, Haml or Slim allow me to write the least amount of code, without even having to introduce a loop variable I wouldn’t be needing later anyway.
Until recently, I would have probably used Haml in this case. However, I’m currently partial to another technique that lets me avoid setting the number of items both in the HTML and CSS preprocessor code, which means I avoid having to modify it in both if I need to use a different value at some point.
To better understand what I mean, consider the following Haml and Sass:
 6.times do .item
$ n: 6; // number of items /* set styles depending on $ n */
In the example above, if I change the number of items in the Haml code, then I need to also change it in the Sass code, otherwise things break. In a more or less obvious manner, the result is not the intended one anymore.
So we can go around that by setting the number of circles as the value of a CSS variable we later use in the Sass code. And, in this situation, I feel better using Pug:
 var nc = 6; // number of circles .watchface(style=`nc: $ {nc}`)  for(var i = 0; i < nc; i++) .circle(style=`i: $ {i}`)
We’ve also set the index for every .circle
element in a similar manner.
The basic styles
We keep the exact same styles on the body
, no change there.
Just like for the structure, we use a preprocessor in order to avoid writing almost the same thing multiple times. My choice is Sass because that’s what I’m most comfortable with, but for something simple like this demo, there’s nothing in particular about Sass that makes it the best choice – LESS or Stylus do the job just as well. It’s just faster for me to write Sass code, that’s all.
But what do we use a preprocessor for?
Well, first of all, we use a variable $ d
for the diameter of the circles, so that if we want to make them bigger or smaller and also control how far out they go during the animation, we only have to change the value of this variable.
In case anyone is wondering why not use CSS variables here, it’s because I prefer to only take this path when I need my variables to be dynamic. This is not the case with the diameter, so why write more and then maybe even have to come up with workarounds for CSS variable bugs we might run into?
$ d: 8em; .circle { width: $ d; height: $ d; }
Note that we are not setting any dimensions on the wrapper (.watchface
). We don’t need to.
In general, if the purpose of an element is just to be a container for absolutely positioned elements, a container on which we apply group transforms (animated or not) and this container has no visible text content, no backgrounds, no borders, no box shadows… then there’s no need to set explicit dimensions on it.
A side effect of this is that, in order to keep our circles in the middle, we need to give them a negative margin
of minus the radius, (which is half the diameter).
$ d: 8em; $ r: .5*$ d; .circle { margin: $ r; width: $ d; height: $ d; }
We also give them the same borderradius
, mixblendmode
and background
as in the original article and we get the following result:
Well, we get the above in WebKit browsers and Firefox, as Edge doesn’t yet support mixblendmode
(though you can vote for implementation and please do that if you want to see it supported because your votes do count), so it shows us something a bit ugly:
To get around this, we use @supports
:
.circle { /* same styles as before */ @supports not (mixblendmode: screen) { opacity: .75 } }
Not perfect, but much better:
Now let’s look a bit at the result we want to get:
We have six circles in total, three of them in the left half and three others in the right half. They all have a background
that’s some kind of green, those in the left half a bit more towards yellow and those in the right half a bit more towards blue.
If we number our circles starting from the topmost one in the right half and then going clockwise, we have that the first three circles are in the right half and have a bluish green background
and the last three are in the left half and have a yellowish green background
.
At this point, we’ve set the background
for all the circles to be the yellowish blue one. This means we need to override it for the first half of the six circles. Since we cannot use CSS variables in selectors, we do this from the Pug code:
 var nc = 6; // number of circles style .circle:nthchild(n + #{.5*nc}) { background: #529ca0 } .watchface(style=`nc: $ {nc}`)  for(var i = 0; i < nc; i++) .circle(style=`i: $ {i}`)
In case you need a refresher on this, :nthchild(n + a)
selects the items at the valid indices we get for n ≥ 0
integer values. In our case, a = .5*nc = .5*6 = 3
, so our selector is :nthchild(n + 3)
.
If we replace n
with 0
, we get 3
, which is a valid index, so our selector matches the third circle.
If we replace n
with 1
, we get 2
, also a valid index, so our selector matches the second circle.
If we replace n
with 2
, we get 1
, again valid, so our selector matches the first circle.
If we replace n
with 3
, we get 0
, which isn’t a valid index, as indices are not 0
based here. At this point, we stop as it becomes clear we won’t be getting any other positive values if we continue.
The following Pen illustrates how this works – the general rule is that :nthchild(n + a)
selects the first a
items:
See the Pen by thebabydino (@thebabydino) on CodePen.
Returning to our circular distribution, the result so far can be seen below:
See the Pen by thebabydino (@thebabydino) on CodePen.
Positioning
First off, we make the wrapper relatively positioned and its .circle
children absolutely positioned. Now they all overlap in the middle.
See the Pen by thebabydino (@thebabydino) on CodePen.
In order to understand what we need to do next, let’s take a look at the following illustration:
The central points of the circles in the initial position are on the same horizontal line and a radius away from the rightmost circle. This means we can get to this final position by a translation of a radius $ r
along the x axis.
But what about the other circles? Their central points in the final position are also a radius away from their initial position, only along other lines.
This means that, if we first rotate their system of coordinates until their x axis coincides with the line between the initial and final position of the central points and then translate them by a radius, we can get them all in the correct final position in a very similar manner.
See the Pen by thebabydino (@thebabydino) on CodePen.
Alright, but rotate each of them by what angle?
Well, we start from the fact that we have 360°
on a circle around a point.
See the Pen by thebabydino (@thebabydino) on CodePen.
We have six circles distributed evenly, so the rotation difference between any two consecutive ones is 360°/6 = 60°
. Since we don’t need to rotate the rightmost .circle
(the second one), that one’s at 0°
, which puts the one before (the first one) at 60°
, the one after (the second one) at 60°
and so on.
See the Pen by thebabydino (@thebabydino) on CodePen.
Note that 60°
and 300° = 360°  60°
occupy the same position on the circle, so whether we get there by a clockwise (positive) rotation of 300°
or by going 60°
the other way around the circle (which gives us the minus sign) doesn’t matter. We’ll be using the 60°
option in the code because it makes it easier to spot a convenient pattern in our case.
So our transforms look like this:
.circle { &:nthchild(1 /* = 0 + 1 */) { transform: rotate(60deg /* 1·60° = (0  1)·360°/6 */) translate($ r); } &:nthchild(2 /* = 1 + 1 */) { transform: rotate( 0deg /* 0·60° = (1  1)·360°/6 */) translate($ r); } &:nthchild(3 /* = 2 + 1 */) { transform: rotate( 60deg /* 1·60° = (2  1)·360°/6 */) translate($ r); } &:nthchild(4 /* = 3 + 1 */) { transform: rotate(120deg /* 2·60° = (3  1)·360°/6 */) translate($ r); } &:nthchild(5 /* = 4 + 1 */) { transform: rotate(180deg /* 3·60° = (4  1)·360°/6 */) translate($ r); } &:nthchild(6 /* = 5 + 1 */) { transform: rotate(240deg /* 4·60° = (5  1)·360°/6 */) translate($ r); } }
This gives us the distribution we’ve been after:
See the Pen by thebabydino (@thebabydino) on CodePen.
However, it’s very repetitive code that can easily be compacted. For any of them, the rotation angle can be written as a function of the current index and the total number of items:
.circle { /* previous styles */ transform: rotate(calc((var(i)  1)*360deg/var(nc))) translate($ r); }
This works in WebKit browsers and Firefox 57+, but fails in Edge and older Firefox browsers due to the lack of support for using calc()
inside rotate()
functions.
Fortunately, in this case, we have the option of computing and setting the individual rotation angles in the Pug code and then using them as such in the Sass code:
 var nc = 6, ba = 360/nc; style .circle:nthchild(n + #{.5*nc}) { background: #529ca0 } .watchface  for(var i = 0; i < nc; i++) .circle(style=`ca: $ {(i  1)*ba}deg`)
.circle { /* previous styles */ transform: rotate(var(ca)) translate($ r); }
We didn’t really need the previous custom properties for anything else in this case, so we just got rid of them.
We now have a compact code, crossbrowser version of the distribution we’ve been after:
See the Pen by thebabydino (@thebabydino) on CodePen.
Good, this means we’re done with the most important part! Now for the fluff…
Finishing up
We take the transform
declaration out of the class and put it inside a set of @keyframes
. In the class, we replace it with the no translation case:
.circle { /* same as before */ transform: rotate(var(ca)) } @keyframes circle { to { transform: rotate(var(ca)) translate($ r) } }
We also add the @keyframes
set for the pulsing animation on the .watchface
element.
@keyframes pulse { 0% { transform: scale(.15) rotate(.5turn) } }
Note that we don’t need both the 0%
(from
) and 100%
(to
) keyframes. Whenever these are missing, their values for the animated properties (just the transform
property in our case) are generated from the values we’d have on the animated elements without the animation
.
In the circle
animation case, that’s rotate(var(ca))
. In the pulse
animation case, scale(1)
gives us the same matrix as none
, which is the default value for transform
so we don’t even need to set it on the .watchface
element.
We make the animationduration
a Sass variable, so that, if we ever want to change it, we only need to change it in one place. And finally, we set the animation
property on both the .watchface
element and the .circle
elements.
$ t: 4s; .watchface { position: relative; animation: pulse $ t cubicbezier(.5, 0, .5, 1) infinite alternate } .circle { /* same as before */ animation: circle $ t infinite alternate }
Note that we’re not setting a timing function for the circle
animation. This is ease
in the original demo and we don’t set it explicitly because it’s the default value.
And that’s it – we have our animated result!
We could also tweak the translation distance so that it’s not exactly $ r
, but a slightly smaller value (something like .95*$ r
for example). This can also make the mixblendmode
effect a bit more interesting:
See the Pen by thebabydino (@thebabydino) on CodePen.
Bonus: the general case!
The above is for six .circle
petals in particular. Now we’ll see how we can adapt it so that it works for any number of petals. Wait, do we need to do more than just change the number of circle elements from the Pug code?
Well, let’s see what happens if we do just that:
The results don’t look bad, but they don’t fully follow the same pattern – having the first half of the circles (the bluish green ones) on the right side of a vertical symmetry line and the second half (yellowish green) on the left side.
We’re pretty close in the nc = 8
case, but the symmetry line isn’t vertical. In the nc = 9
case however, all our circles have a yellowish green background
.
So let’s see why these things happen and how we can get the results we actually want.
Making :nthchild()
work for us
First off, remember we’re making half the number of circles have a bluish green background
with this little bit of code:
.circle:nthchild(n + #{.5*nc}) { background: #529ca0 }
But in the nc = 9
case, we have that .5*nc = .5*9 = 4.5
, which makes our selector :nthchild(n + 4.5)
. Since 4.5
is not an integer, the selector isn’t valid and the background
doesn’t get applied. So the first thing we do here is floor the .5*nc
value:
style .circle:nthchild(n + #{~~(.5*nc)}) { background: #529ca0 }
This is better, as for a nc
value of 9
, the selector we get is .circle:nthchild(n + 4)
, which gets us the first 4
items to apply a bluish green background
on them:
See the Pen by thebabydino (@thebabydino) on CodePen.
However, we still don’t have the same number of bluish green and yellowish green circles if nc
is odd. In order to fix that, we make the circle in the middle (going from the first to the last) have a gradient background
.
By “the circle in the middle” we mean the circle that’s an equal number of circles away from both the start and the end. The following interactive demo illustrates this, as well as the fact that, when the total number of circles is even, we don’t have a middle circle.
See the Pen by thebabydino (@thebabydino) on CodePen.
Alright, how do we get this circle?
Mathematically, this is the intersection between the set containing the first ceil(.5*nc)
items and the set containing all but the first floor(.5*nc)
items. If nc
is even, then floor(.5*nc)
and ceil(.5*nc)
are equal and our intersection is the empty set ∅
. This is illustrated by the following Pen:
See the Pen by thebabydino (@thebabydino) on CodePen.
We get the first ceil(.5*nc)
items using :nthchild(n + #{Math.ceil(.5*nc)})
, but what about the other set?
In general, :nthchild(n + a)
selects all but the first a  1
items:
See the Pen by thebabydino (@thebabydino) on CodePen.
So in order to get all but the first floor(.5*nc)
items, we use :nthchild(n + #{~~(.5*nc) + 1})
.
This means we have the following selector for the middle circle:
:nthchild(n + #{~~(.5*nc) + 1}):nthchild(n + #{Math.ceil(.5*nc)})
Let’s see what this gives us.
 If we have
3
items, our selector is:nthchild(n + 2):nthchild(n + 2)
, which gets us the second item (the intersection between the{2, 3, 4, ...}
and{2, 1}
sets)  If we have
4
items, our selector is:nthchild(n + 3):nthchild(n + 2)
, which doesn’t catch anything (the intersection between the{3, 4, 5, ...}
and{2, 1}
sets is the empty set∅
)  If we have
5
items, our selector is:nthchild(n + 3):nthchild(n + 3)
, which gets us the third item (the intersection between the{3, 4, 5, ...}
and{3, 2, 1}
sets)  If we have
6
items, our selector is:nthchild(n + 4):nthchild(n + 3)
, which doesn’t catch anything (the intersection between the{4, 5, 6, ...}
and{3, 2, 1}
sets is the empty set∅
)  If we have
7
items, our selector is:nthchild(n + 4):nthchild(n + 4)
, which gets us the fourth item (the intersection between the{4, 5, 6, ...}
and{4, 3, 2, 1}
sets)  If we have
8
items, our selector is:nthchild(n + 5):nthchild(n + 4)
, which doesn’t catch anything (the intersection between the{5, 6, 7, ...}
and{4, 3, 2, 1}
sets is the empty set∅
)  If we have
9
items, our selector is:nthchild(n + 5):nthchild(n + 5)
, which gets us the fifth item (the intersection between the{5, 6, 7, ...}
and{5, 4, 3, 2, 1}
sets)
Now that we can select the item in the middle when we have an odd number of them in total, let’s give it a gradient background
:
 var nc = 6, ba = 360/nc; style .circle:nthchild(n + #{~~(.5*nc)}) { background: var(c0) }  .circle:nthchild(n + #{~~(.5*nc) + 1}):nthchild(n + #{Math.ceil(.5*nc)}) {  background: lineargradient(var(c0), var(c1))  } .watchface(style=`c0: #529ca0; c1: #61bea2`)  for(var i = 0; i < nc; i++) .circle(style=`ca: $ {(i  1)*ba}deg`)
The reason why we use a top to bottom gradient is that, ultimately, we want this item to be at the bottom, split into two halves by the vertical symmetry line of the assembly. This means we first need to rotate it until its x axis points down and then translate it down along this new direction of its x axis. In this position, the top of the item is in the right half of the assembly and the bottom of the item is in the left half of the assembly. So, if we want a gradient from the right side of the assembly to the left side of the assembly, this is a top to bottom gradient on that actual .circle
element.
See the Pen by thebabydino (@thebabydino) on CodePen.
Using this technique, we have now solved the issue of the backgrounds for the general case:
See the Pen by thebabydino (@thebabydino) on CodePen.
Now all that’s left to do is make the symmetry axis vertical.
Taming the angles
In order to see what we need to do here, let’s focus on the desired positioning in the top part. There, we want to always have two circles (the first in DOM order on the right and the last in DOM order on the left) symmetrically positioned with respect to the vertical axis that splits our assembly into two halves that mirror each other.
See the Pen by thebabydino (@thebabydino) on CodePen.
The fact that they’re symmetrical means the vertical axis splits the angular distance between them ba
(which is 360°
divided by the total number of circles nc
) into two equal halves.
So both are half a base angle (where the base angle ba
is 360°
divided by the total number of circles nc
) away from the vertical symmetry axis, one in the clockwise direction and the other one the other way.
The upper half of the symmetry axis is at 90°
(which is equivalent to 270°
).
So in order to get to the first circle in DOM order (the one at the top on the right), we start from 0°
, go by 90°
in the negative direction and then by half a base angle back in the positive direction (clockwise). This puts the first circle at .5*ba  90
degrees.
After that, every other circle is at the angle of the previous circle plus a base angle. This way, we have:
 the first circle (index
0
, selector:nthchild(1)
) is atca₀ = .5*ba  90
degrees  the second circle (index
1
, selector:nthchild(2)
) is atca₁ = ca₀ + ba = ca₀ + 1*ba
degrees  the third circle (index
2
, selector:nthchild(3)
u) is atca₂ = ca₁ + ba = ca₀ + ba + ba = ca₀ + 2*ba
degrees  in general, the circle of index
k
is atcaₖ = caₖ₋₁ + ba = ca₀ + k*ba
degrees
So the the current angle of the circle at index i
is .5*ba  90 + i*ba = (i + .5)*ba  90
degrees:
 var nc = 6, ba = 360/nc; // same as before .watchface(style=`c0: #529ca0; c1: #61bea2`)  for(var i = 0; i < nc; i++) .circle(style=`ca: $ {(i + .5)*ba  90}deg`)
This gives our final Pen, where we only need to change nc
from the Pug code to change the result:
See the Pen by thebabydino (@thebabydino) on CodePen.
Simplifying the Apple Watch Breathe App Animation With CSS Variables is a post from CSSTricks
Comments
Related Posts

A Sliding Nightmare: Understanding the Range Input
No Comments  Dec 31, 2017

Lots of ways to add an ID to the `body` element
No Comments  Oct 11, 2016

Designing with retro colors: showcase and tips
No Comments  Mar 9, 2018

Declarative Data Fetching with GraphQL
No Comments  Oct 13, 2016