1 Element CSS Rainbow Gradient Infinity

I first got the idea to CSS something of the kind when I saw this gradient infinity logo by Infographic Paradise:

Original illustration. Shows a thick infinity symbol with a rainbow gradient filling its two loops and some highlights over this gradient.
The original gradient infinity.

After four hours and some twenty minutes, of which over four hours were spent on tweaking positioning, edges and highlights… I finally had the result below:

Screenshot of my version. Shows a thick infinity symbol with a rainbow gradient filling its two loops and some highlights over this gradient.
My version of the rainbow gradient infinity.

The gradient doesn’t look like in the original illustration, as I chose to generate the rainbow logically instead of using the Dev Tools picker or something like that, but other than that, I think I got pretty close—let’s see how I did that!

Markup

As you’ve probably already guessed from the title, the HTML is just one element:

<div class='∞'></div>

Styling

Deciding on the approach

The first idea that might come to mind when seeing the above would be using conic gradients as border images. Unfortunately, border-image and border-radius don’t play well together, as illustrated by the interactive demo below:

See the Pen by thebabydino (@thebabydino) on CodePen.

Whenever we set a border-image, border-radius just gets ignored, so using the two together is sadly not an option.

So the approach we take here is using conic-gradient() backgrounds and then getting rid of the part in the middle with the help of a mask. Let’s see how that works!

Creating the two ∞ halves

We first decide on an outer diameter.

$do: 12.5em;

We create the two halves of the infinity symbol using the ::before and ::after pseudo-elements of our .∞ element. In order to place these two pseudo-elements next to one another, we use a flex layout on their parent (the infinity element .∞). Each of these has both the width and the height equal to the outer diameter $do. We also round them with a border-radius of 50% and we give them a dummy background so we can see them.

.∞ { display: flex; &:before, &:after { width: $do; height: $do; border-radius: 50%; background: #000; content: ''; }
}

We’ve also placed the .∞ element in the middle of its parent (the body in this case) both vertically and horizontally by using the flexbox approach.

See the Pen by thebabydino (@thebabydino) on CodePen.

How conic-gradient() works

In order to create the conic-gradient() backgrounds for the two haves, we must first understand how the conic-gradient() function works.

If inside the conic-gradient() function we have a list of stops without explicit positions, then the first is taken to be at 0% (or 0deg, same thing), the last is taken to be at 100% (or 360deg), while all those left are distributed evenly in the [0%, 100%] interval.

See the Pen by thebabydino (@thebabydino) on CodePen.

If we have just 2 stops, it’s simple. The first is at 0%, the second (and last) at 100% and there are no other stops in between.

If we have 3 stops, the first is at 0%, the last (third) at 100%, while the second is dead in the middle of the [0%, 100%] interval, at 50%.

If we have 4 stops, the first is at 0%, the last (fourth) at 100%, while the second and third split the [0%, 100%] interval into 3 equal intervals, being positioned at 33.(3)% and 66.(6)% respectively.

If we have 5 stops, the first is at 0%, the last (fifth) at 100%, while the second, third and fourth split the [0%, 100%] interval into 4 equal intervals being positioned at 25%, 50% and 75% respectively.

If we have 6 stops, the first is at 0%, the last (sixth) at 100%, while the second, third, fourth and fifth split the [0%, 100%] interval into 5 equal intervals being positioned at 20%, 40%, 60% and 80% respectively.

In general, if we have n stops, the first is at 0%, the last at 100%, while the ones in between split the [0%, 100%] interval into n-1 eqial intervals spanning 100%/(n-1) each. If we give the stops 0-based indices, then each one of them is positioned at i*100%/(n-1).

For the first one, i is 0, which gives us 0*100%/(n-1) = 0%.

For the last (n-th) one, i is n-1, which gives us (n-1)*100%/(n-1) = 100%.

Here, we choose to use 9 stops which means we split the [0%, 100%] interval into 8 equal intervals.

Alright, but how do we get the stop list?

The hsl() stops

Well, for simplicity, we choose to generate it as a list of HSL values. We keep the saturation and the lightness fixed and we vary the hue. The hue is an angle value that goes from 0 to 360, as we can see here:

Hue scale from 0 to 360 in the HSB/HSL models.
Visual representation of the hue scale from 0 to 360 (saturation and lightness being kept constant).

With this in mind, we can construct a list of hsl() stops with fixed saturation and lightness and varying hue if we know the start hue $hue-start, the hue range $hue-range (this is the end hue minus the start hue) and the number of stops $num-stops.

Let’s say we keep the saturation and the lightness fixed at 85% and 57%, respectively (arbitrary values that can probably be tweaked for better results) and, for example, we might go from a start hue of 240 to an end hue of 300 and use 4 stops.

In order to generate this list of stops, we use a get-stops() function that takes these three things as arguments:

@function get-stops($hue-start, $hue-range, $num-stops) {}

We create the list of stops $list which is originally empty (and which we’ll return at the end after we populate it). We also compute the span of one of the equal intervals our stops split the full start to end interval into ($unit).

@function get-stops($hue-start, $hue-range, $num-stops) { $list: (); $unit: $hue-range/($num-stops - 1); /* populate the list of stops $list */ @return $list
}

In order to populate our $list, we loop through the stops, compute the current hue, use the current hue to generate the hsl() value at that stop and then then add it to the list of stops:

@for $i from 0 to $num-stops { $hue-curr: $hue-start + $i*$unit; $list: $list, hsl($hue-curr, 85%, 57%);
}

We can now use the stop list this function returns for any kind of gradient, as it can be seen from the usage examples for this function shown in the interactive demo below (navigation works both by using the previous/next buttons on the sides as well as the arrow keys and the PgDn/ PgUp keys):

See the Pen by thebabydino (@thebabydino) on CodePen.

Note how, when our range passes one end of the [0, 360] interval, it continues from the other end. For example, when the start hue is 30 and the range is -210 (the fourth example), we can only go down to 0, so then we continue going down from 360.

Conic gradients for our two halves

Alright, but how do we determine the $hue-start and the $hue-range for our particular case?

In the original image, we draw a line in between the central points of the two halves of the loop and, starting from this line, going clockwise in both cases, we see where we start from and where we end up in the [0, 360] hue interval and what other hues we pass through.

Original illustration, annotated. We've marked out the central points of the two halves, connected them with a line and used this line as the start for going around each of the two halves in the clockwise direction.
We start from the line connecting the central points of the two halves and we go around them in the clockwise direction.

To simplify things, we consider we pass through the whole [0, 360] hue scale going along our infinity symbol. This means the range for each half is 180 (half of 360) in absolute value.

Hue scale from 0 to 360 in the HSB/HSL models, with saturation and lightness fixed at 100% and 50% respectively. Red corresponds to a hue of 0/ 360, yellow to a hue of 60, lime to a hue of 120, cyan to a hue of 180, blue to a hue of 240, magenta to a hue of 300.
Keywords to hue values correspondence for saturation and lightness fixed at 100% and 50% respectively.

On the left half, we start from something that looks like it’s in between some kind of cyan (hue 180) and some kind of lime (hue 120), so we take the start hue to be the average of the hues of these two (180 + 120)/2 = 150.

Original illustration, annotated. For the left half, our start hue is 150 (something between a kind of cyan and a kind of lime), we pass through yellows, which are around 60 in hue and end up at a kind of red, 180 away from the start, so at 330.
The plan for the left half.

We get to some kind of red, which is 180 away from the start value, so at 330, whether we subtract or add 180:

(150 - 180 + 360)%360 = (150 + 180 + 360)%360 = 330

So… do we go up or down? Well, we pass through yellows which are around 60 on the hue scale, so that’s going down from 150, not up. Going down means our range is negative (-180).

Original illustration, annotated. For the right half, our start hue is 150 (something between a kind of cyan and a kind of lime), we pass through blues, which are around 240 in hue and end up at a kind of red, 180 away from the start, so at 330.
The plan for the right half.

On the right half, we also start from the same hue in between cyan and lime (150) and we also end at the same kind of red (330), but this time we pass through blues, which are around 240, meaning we go up from our start hue of 150, so our range is positive in this case (180).

As far as the number of stops goes, 9 should suffice.

Now update our code using the values for the left half as the defaults for our function:

@function get-stops($hue-start: 150, $hue-range: -180, $num-stops: 9) { /* same as before */
} .∞ { display: flex; &:before, &:after { /* same as before */ background: conic-gradient(get-stops()); } &:after { background: conic-gradient(get-stops(150, 180)); }
}

And now our two discs have conic-gradient() backgrounds:

See the Pen by thebabydino (@thebabydino) on CodePen.

However, we don’t want these conic gradients to start from the top.

For the first disc, we want it to start from the right—that’s at 90° from the top in the clockwise (positive) direction. For the second disc, we want it to start from the left—that’s at 90° from the top in the other (negative) direction, which is equivalent to 270° from the top in the clockwise direction (because negative angles don’t appear to work from some reason).

The conic gradient for the first (left) half starts from the right, which means an offset of 90° in the clockwise (positive) direction from the top. The conic gradient for the second (right) half starts from the left, which means an offset of 270° in the clockwise (positive) direction (and of 90° in the negative direction) from the top.
Angular offsets from the top for our two halves.

Let’s modify our code to achieve this:

.∞ { display: flex; &:before, &:after { /* same as before */ background: conic-gradient(from 90deg, get-stops()); } &:after { background: conic-gradient(from 270deg, get-stops(150, 180)); }
}

So far, so good!

See the Pen by thebabydino (@thebabydino) on CodePen.

From 🥧 to 🍩

The next step is to cut holes out of our two halves. We do this with a mask or, more precisely, with a radial-gradient() one. This cuts out Edge support for now, but since it’s something that’s in development, it’s probably going to be a cross-browser solution at some point in the not too far future.

Remember that CSS gradient masks are alpha masks by default (and only Firefox currently allows changing this via mask-mode), meaning that only the alpha channel matters. Overlaying the mask over our element makes every pixel of this element use the alpha channel of the corresponding pixel of the mask. If the mask pixel is completely transparent (its alpha value is 0), then so will the corresponding pixel of the element.

See the Pen by thebabydino (@thebabydino) on CodePen.

In order to create the mask, we compute the outer radius $ro (half the outer diameter $do) and the inner radius $ri (a fraction of the outer radius $ro).

$ro: .5*$do;
$ri: .52*$ro;
$m: radial-gradient(transparent $ri, red 0);

We then set the mask on our two halves:

.∞ { /* same as before */ &:before, &:after { /* same as before */ mask: $m; }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

This looks perfect in Firefox, but the edges of radial gradients with abrupt transitions from one stop to another look ugly in Chrome and, consequently, so do the inner edges of our rings.

Screenshot. Shows a close-up of the inner edge of the right half in Chrome. These inner edges look jagged and ugly in Chrome.
Close-up of the inner edge of the right half in Chrome.

The fix here would be not to have an abrupt transition between stops, but spread it out over a small distance, let’s say half a pixel:

$m: radial-gradient(transparent calc(#{$ri} - .5px), red $ri);

We now got rid of the jagged edges in Chrome:

Screenshot. Shows a close-up of the inner edge of the right half in Chrome after spreading out the transition between stops over half a pixel. These inner edges now look blurry and smoother in Chrome.
Close-up of the inner edge of the right half in Chrome after spreading out the transition between stops over half a pixel.

The following step is to offset the two halves such that they actually form an infinity symbol. The visible circular strips both have the same width, the difference between the outer radius $ro and the inner radius $ri. This means we need to shift each laterally by half this difference $ri - $ri.

.∞ { /* same as before */ &:before, &:after { /* same as before */ margin: 0 (-.5*($ro - $ri)); }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

Intersecting halves

We’re getting closer, but we still have a very big problem here. We don’t want the right part of the loop to be completely over the left one. Instead, we want the top half of the right part to be over that of the left part and the bottom half of the left part to be over that of the right part.

So how do we achieve that?

We take a similar approach to that presented in an older article: using 3D!

In order to better understand how this works, consider the two card example below. When we rotate them around their x axes, they’re not in the plane of the screen anymore. A positive rotation brings the bottom forward and pushes the top back. A negative rotation brings the top forward and pushes the bottom back.

See the Pen by thebabydino (@thebabydino) on CodePen.

Note that the demo above doesn’t work in Edge.

So if we give the left one a positive rotation and the right one a negative rotation, then the top half of the right one appears in front of the top half of the left one and the other way around for the bottom halves.

Addiing perspective makes what’s closer to our eyes appears bigger and what’s further away appears smaller and we use way smaller angles. Without it, we have the 3D plane intersection without the 3D appearance.

Note that both our halves need to be in the same 3D context, something that’s achieved by setting transform-style: preserve-3d on the .∞ element.

.∞ { /* same as before */ transform-style: preserve-3d; &:before, &:after { /* same as before */ transform: rotatex(1deg); } &:after { /* same as before */ transform: rotatex(-1deg); }
}

And now we’re almost there, but not quite:

See the Pen by thebabydino (@thebabydino) on CodePen.

Fine tuning

We have a little reddish strip in the middle because the gradient ends and the intersection line don’t quite match:

Screenshot. Shows a close-up of the intersection of the two halves. In theory, the intersection line should match the start/ end line of the conic gradients, but this isn't the case in practice, so we're still seeing a strip of red along it, even though the red side should be behind the plane of the screen and not visible.
Close-up of small issue at the intersection of the two halves.

A pretty ugly, but efficient fix is to add a 1px translation before the rotation on the right part (the ::after pseudo-element):

.∞:after { transform: translate(1px) rotatex(-1deg) }

Much better!

See the Pen by thebabydino (@thebabydino) on CodePen.

This still isn’t perfect though. Since the inner edges of our two rings are a bit blurry, the transition in between them and the crisp outer ones looks a bit odd, so maybe we can do better there:

Screenshot. Shows a close-up of the area around the intersection of the two halves, where the crisp outer edges meet the blurry inner ones, which looks odd.
Close-up of continuity issue (crisp outer edges meeting blurry inner ones).

A quick fix here would be to add a radial-gradient() cover on each of the two halves. This cover is transparent white for most of the unmasked part of the two halves and goes to solid white along both their inner and outer edges such that we have nice continuity:

$gc: radial-gradient(#fff $ri, rgba(#fff, 0) calc(#{$ri} + 1px), rgba(#fff, 0) calc(#{$ro} - 1px), #fff calc(#{$ro} - .5px)); .∞ { /* same as before */ &:before, &:after { /* same as before */ background: $gc, conic-gradient(from 90deg, get-stops()); } &:after { /* same as before */ background: $gc, conic-gradient(from 270deg, get-stops(150, 180)); }
}

The benefit becomes more obvious once we add a dark background to the body:

See the Pen by thebabydino (@thebabydino) on CodePen.

Now it looks better even when zooming in:

Screenshot. Shows a close-up of the area around the intersection of the two halves, we don't have the same sharp contrast between inner and outer edges, not even when zooming in.
No more sharp contrast between inner and outer edges.

The final result

Finally, we add some prettifying touches by layering some more subtle radial gradient highlights over the two halves. This was the part that took me the most because it involved the least amount of logic and the most amount of trial and error. At this point, I just layered the original image underneath the .∞ element, made the two halves semi-transparent and started adding gradients and tweaking them until they pretty much matched the highlights. And you can see when I got sick of it because that’s when the position values become rougher approximations with fewer decimals.

Another cool touch would be drop shadows on the whole thing using a filter on the body. Sadly, this breaks the 3D intersection effect in Firefox, which means we cannot add it there, too.

@supports not (-moz-transform: scale(2)) { filter: drop-shadow(.25em .25em .25em #000) drop-shadow(.25em .25em .5em #000);
}

We now have the final static result!

See the Pen by thebabydino (@thebabydino) on CodePen.

Spicing it up with animation!

When I first shared this demo, I got asked about animating it. I initially thought this would be complicated, but then it hit me that, thanks to Houdini, it doesn’t have to be!

As mentioned in my previous article, we can animate in between stops, let’s say from a red to a blue. In our case, the saturation and lightness components of the hsl() values used to generate the rainbow gradient stay constant, all that changes is the hue.

For each and every stop, the hue goes from its initial value to its initial value plus 360, thus passing through the whole hue scale in the process. This is equivalent to keeping the initial hue constant and varying an offset. This offset --off is the custom property we animate.

Sadly, this means support is limited to Blink browsers with the Experimental Web Platform features flag enabled.

Screenshot showing the Experimental Web Platform features flag being enabled in Chrome.
The Experimental Web Platform features flag enabled in Chrome.

Still, let’s see how we put it all into code!

For starters, we modify the get-stops() function such that the current hue at any time is the initial hue of the current stop $hue-curr plus our offset --off:

$list: $list, hsl(calc(#{$hue-curr} + var(--off, 0)), 85%, 57%);

Next, we register this custom property:

CSS.registerProperty({ name: '--off', syntax: '<number>', initialValue: 0;
})

And finally, we animate it to 360:

.∞ { /* same as before */ &:before, &:after { /* same as before */ animation: shift 2s linear infinite; }
} @keyframes shift { to { --off: 360 } }

This gives us our animated gradient infinity!

Animated ∞ logo (live demo, Blink only with flag enabled).

That’s it! I hope you’ve enjoyed this dive into what can be done with CSS these days!

The post 1 Element CSS Rainbow Gradient Infinity appeared first on CSS-Tricks.

A Strategy Guide To CSS Custom Properties

CSS preprocessor variables and CSS custom properties (often referred to as “CSS variables”) can do some of the same things, but are not the same.

Practical advice from Mike Riethmuller:

If it is alright to use static variables inside components, when should we use custom properties? Converting existing preprocessor variables to custom properties usually makes little sense. After all, the reason for custom properties is completely different. Custom properties make sense when we have CSS properties that change relative to a condition in the DOM — especially a dynamic condition such as :focus, :hover, media queries or with JavaScript.

Direct Link to Article — Permalink

The post A Strategy Guide To CSS Custom Properties appeared first on CSS-Tricks.

1 HTML Element + 5 CSS Properties = Magic!

Let’s say I told you we can get the results below with just one HTML element and five CSS properties for each. No SVG, no images (save for the background on the root that’s there just to make clear that our one HTML element has some transparent parts), no JavaScript. What would you think that involves?

Screenshots. On the left, a screenshot of equal radial slices of a pie with transparent slices (gaps) in between them. The whole assembly has a top to bottom gradient (orange to purple). On the right, the XOR operation between what we have on the left and a bunch of concentric ripples. Again, the whole assembly has the same top to bottom gradient.
The desired results.

Well, this article is going to explain just how to do this and then also show how to make things fun by adding in some animation.

CSS-ing the Gradient Rays

The HTML is just one <div>.

<div class='rays'></div>

In the CSS, we need to set the dimensions of this element and we need to give it a background so that we can see it. We also make it circular using border-radius:

.rays { width: 80vmin; height: 80vmin; border-radius: 50%; background: linear-gradient(#b53, #f90);
}

And… we’ve already used up four out of five properties to get the result below:

See the Pen by thebabydino (@thebabydino) on CodePen.

So what’s the fifth? mask with a repeating-conic-gradient() value!

Let’s say we want to have 20 rays. This means we need to allocate $p: 100%/20 of the full circle for a ray and the gap after it.

Illustration. Shows how we slice the disc to divide it into equal rays and gaps.
Dividing the disc into rays and gaps (live).

Here we keep the gaps in between rays equal to the rays (so that’s .5*$p for either a ray or a space), but we can make either of them wider or narrower. We want an abrupt change after the ending stop position of the opaque part (the ray), so the starting stop position for the transparent part (the gap) should be equal to or smaller than it. So if the ending stop position for the ray is .5*$p, then the starting stop position for the gap can’t be bigger. However, it can be smaller and that helps us keep things simple because it means we can simply zero it.

SVG illustration. Connects the stop positions from the code to the actual corresponding points on the circle defining the repeating conic gradient.
How repeating-conic-gradient() works (live).
$nr: 20; // number of rays
$p: 100%/$nr; // percent of circle allocated to a ray and gap after .rays { /* same as before */ mask: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p);
}

Note that, unlike for linear and radial gradients, stop positions for conic gradients cannot be unitless. They need to be either percentages or angular values. This means using something like transparent 0 $p doesn’t work, we need transparent 0% $p (or 0deg instead of 0%, it doesn’t matter which we pick, it just can’t be unitless).

Screenshot of equal radial slices of a pie with transparent slices (gaps) in between them. The whole assembly has a top to bottom gradient (orange to purple).
Gradient rays (live demo, no Edge support).

There are a few things to note here when it comes to support:

  • Edge doesn’t support masking on HTML elements at this point, though this is listed as In Development and a flag for it (that doesn’t do anything for now) has already shown up in about:flags.
    Screenshot showing the about:flags page in Edge, with the 'Enable CSS Masking' flag highlighted.
    The Enable CSS Masking flag in Edge.
  • conic-gradient() is only supported natively by Blink browsers behind the Experimental Web Platform features flag (which can be enabled from chrome://flags or opera://flags). Support is coming to Safari as well, but, until that happens, Safari still relies on the polyfill, just like Firefox.
    Screenshot showing the Experimental Web Platform Features flag being enabled in Chrome.
    The Experimental Web Platform features flag enabled in Chrome.
  • WebKit browsers still need the -webkit- prefix for mask properties on HTML elements. You’d think that’s no problem since we’re using the polyfill which relies on -prefix-free anyway, so, if we use the polyfill, we need to include -prefix-free before that anyway. Sadly, it’s a bit more complicated than that. That’s because -prefix-free works via feature detection, which fails in this case because all browsers do support mask unprefixed… on SVG elements! But we’re using mask on an HTML element here, so we’re in the situation where WebKit browsers need the -webkit- prefix, but -prefix-free won’t add it. So I guess that means we need to add it manually:
    $nr: 20; // number of rays
    $p: 100%/$nr; // percent of circle allocated to a ray and gap after
    $m: repeating-conic-gradient(#000 0% .5*$p, transparent 0% $p); // mask .rays { /* same as before */ -webkit-mask: $m; mask: $m;
    }

    I guess we could also use Autoprefixer, even if we need to include -prefix-free anyway, but using both just for this feels a bit like using a shotgun to kill a fly.

Adding in Animation

One cool thing about conic-gradient() being supported natively in Blink browsers is that we can use CSS variables inside them (we cannot do that when using the polyfill). And CSS variables can now also be animated in Blink browsers with a bit of Houdini magic (we need the Experimental Web Platform features flag to be enabled for that, but we also need it enabled for native conic-gradient() support, so that shouldn’t be a problem).

In order to prepare our code for the animation, we change our masking gradient so that it uses variable alpha values:

$m: repeating-conic-gradient( rgba(#000, var(--a)) 0% .5*$p, rgba(#000, calc(1 - var(--a))) 0% $p);

We then register the alpha --a custom property:

CSS.registerProperty({ name: '--a', syntax: '<number>', initialValue: 1;
})

And finally, we add in an animation in the CSS:

.rays { /* same as before */ animation: a 2s linear infinite alternate;
} @keyframes a { to { --a: 0 } }

This gives us the following result:

Animated gif. We animate the alpha of the gradient stops, such that the rays go from fully opaque to fully transparent, effectively becoming gaps, while the opposite happens for the initial gaps, they go from fully transparent to fully opaque, thus becoming rays. At any moment, the alpha of either of them is 1 minus the alpha of the other, so they complement each other.
Ray alpha animation (live demo, only works in Blink browsers with the Experimental Web Platform features flag enabled).

Meh. Doesn’t look that great. We could however make things more interesting by using multiple alpha values:

$m: repeating-conic-gradient( rgba(#000, var(--a0)) 0%, rgba(#000, var(--a1)) .5*$p, rgba(#000, var(--a2)) 0%, rgba(#000, var(--a3)) $p);

The next step is to register each of these custom properties:

for(let i = 0; i < 4; i++) { CSS.registerProperty({ name: `--a${i}`, syntax: '<number>', initialValue: 1 - ~~(i/2) })
}

And finally, add the animations in the CSS:

.rays { /* same as before */ animation: a 2s infinite alternate; animation-name: a0, a1, a2, a3; animation-timing-function: /* easings from easings.net */ cubic-bezier(.57, .05, .67, .19) /* easeInCubic */, cubic-bezier(.21, .61, .35, 1); /* easeOutCubic */
} @for $i from 0 to 4 { @keyframes a#{$i} { to { --a#{$i}: #{floor($i/2)} } }
}

Note that since we’re setting values to custom properties, we need to interpolate the floor() function.

Animated gif. This time, the alpha of each and every stop (start and end of ray, start and end of gap) is animated independently via its own CSS variable. The alphas at the start and end of the ray both go from 1 to 0, but using different timing functions. The alphas at the start and end of the gap both go from 0 to 1, but, again, using different timing functions.
Multiple ray alpha animations (live demo, only works in Blink browsers with the Experimental Web Platform features flag enabled).

It now looks a bit more interesting, but surely we can do better?

Let’s try using a CSS variable for the stop position between the ray and the gap:

$m: repeating-conic-gradient(#000 0% var(--p), transparent 0% $p);

We then register this variable:

CSS.registerProperty({ name: '--p', syntax: '<percentage>', initialValue: '0%'
})

And we animate it from the CSS using a keyframe animation:

.rays { /* same as before */ animation: p .5s linear infinite alternate
} @keyframes p { to { --p: #{$p} } }

The result is more interesting in this case:

Animated gif. The stop position between the ray an the gap animates from 0 (when the ray is basically reduced to nothing) to the whole percentage $p allocated for a ray and the gap following it (which basically means we don't have a gap anymore) and then back to 0 again.
Alternating ray size animation (live demo, only works in Blink browsers with the Experimental Web Platform features flag enabled).

But we can still spice it up a bit more by flipping the whole thing horizontally in between every iteration, so that it’s always flipped for the reverse ones. This means not flipped when --p goes from 0% to $p and flipped when --p goes back from $p to 0%.

The way we flip an element horizontally is by applying a transform: scalex(-1) to it. Since we want this flip to be applied at the end of the first iteration and then removed at the end of the second (reverse) one, we apply it in a keyframe animation as well—in one with a steps() timing function and double the animation-duration.

 $t: .5s; .rays { /* same as before */ animation: p $t linear infinite alternate, s 2*$t steps(1) infinite;
} @keyframes p { to { --p: #{$p} } } @keyframes s { 50% { transform: scalex(-1); } }

Now we finally have a result that actually looks pretty cool:

Animated gif. We have the same animation as before, plus a horizontal flip at the end of every iteration which creates the illusion of a circular sweep instead of just increasing and then decreasing rays, as the rays seems to now decrease from the start after they got to their maximum size incresing from the end.
Alternating ray size animation with horizontal flip in between iterations (live demo, only works in Blink browsers with the Experimental Web Platform features flag enabled).

CSS-ing Gradient Rays and Ripples

To get the rays and ripples result, we need to add a second gradient to the mask, this time a repeating-radial-gradient().

SVG illustration. Connects the stop positions from the code to the actual corresponding points on the circle defining the repeating radial gradient.
How repeating-radial-gradient() works (live).
$nr: 20;
$p: 100%/$nr;
$stop-list: #000 0% .5*$p, transparent 0% $p;
$m: repeating-conic-gradient($stop-list), repeating-radial-gradient(closest-side, $stop-list); .rays-ripples { /* same as before */ mask: $m;
}

Sadly, using multiple stop positions only works in Blink browsers with the same Experimental Web Platform features flag enabled. And while the conic-gradient() polyfill covers this for the repeating-conic-gradient() part in browsers supporting CSS masking on HTML elements, but not supporting conic gradients natively (Firefox, Safari, Blink browsers without the flag enabled), nothing fixes the problem for the repeating-radial-gradient() part in these browsers.

This means we’re forced to have some repetition in our code:

$nr: 20;
$p: 100%/$nr;
$stop-list: #000, #000 .5*$p, transparent 0%, transparent $p;
$m: repeating-conic-gradient($stop-list), repeating-radial-gradient(closest-side, $stop-list); .rays-ripples { /* same as before */ mask: $m;
}

We’re obviously getting closer, but we’re not quite there yet:

Screenshot. We have the same radial slices with equal gaps in between, and over them, a layer of ripples - concentric rings with gaps equal to their width in between them. The whole thing has a top to bottom gradient (orange to purple) with transparent parts where the gaps of the two layers intersect.
Intermediary result with the two mask layers (live demo, no Edge support).

To get the result we want, we need to use the mask-composite property and set it to exclude:

$m: repeating-conic-gradient($stop-list) exclude, repeating-radial-gradient(closest-side, $stop-list);

Note that mask-composite is only supported in Firefox 53+ for now, though Edge should join in when it finally supports CSS masking on HTML elements.

Screenshot. We have the same result as before, except now we have performed a XOR operation between the two layers (rays and ripples).
XOR rays and ripples (live demo, Firefox 53+ only).

If you think it looks like the rays and the gaps between the rays are not equal, you’re right. This is due to a polyfill issue.

Adding in Animation

Since mask-composite only works in Firefox for now and Firefox doesn’t yet support conic-gradient() natively, we cannot put CSS variables inside the repeating-conic-gradient() (because Firefox still falls back on the polyfill for it and the polyfill doesn’t support CSS variable usage). But we can put them inside the repeating-radial-gradient() and even if we cannot animate them with CSS keyframe animations, we can do so with JavaScript!

Because we’re now putting CSS variables inside the repeating-radial-gradient(), but not inside the repeating-conic-gradient() (as the XOR effect only works via mask-composite, which is only supported in Firefox for now and Firefox doesn’t support conic gradients natively, so it falls back on the polyfill, which doesn’t support CSS variable usage), we cannot use the same $stop-list for both gradient layers of our mask anymore.

But if we have to rewrite our mask without a common $stop-list anyway, we can take this opportunity to use different stop positions for the two gradients:

// for conic gradient
$nc: 20;
$pc: 100%/$nc;
// for radial gradient
$nr: 10;
$pr: 100%/$nr;

The CSS variable we animate is an alpha --a one, just like for the first animation in the rays case. We also introduce the --c0 and --c1 variables because here we cannot have multiple positions per stop and we want to avoid repetition as much as possible:

$m: repeating-conic-gradient(#000 .5*$pc, transparent 0% $pc) exclude, repeating-radial-gradient(closest-side, var(--c0), var(--c0) .5*$pr, var(--c1) 0, var(--c1) $pr); body { --a: 0; /* layout, backgrounds and other irrelevant stuff */
} .xor { /* same as before */ --c0: #{rgba(#000, var(--a))}; --c1: #{rgba(#000, calc(1 - var(--a)))}; mask: $m;
}

The alpha variable --a is the one we animate back and forth (from 0 to 1 and then back to 0 again) with a little bit of vanilla JavaScript. We start by setting a total number of frames NF the animation happens over, a current frame index f and a current animation direction dir:

const NF = 50; let f = 0, dir = 1;

Within an update() function, we update the current frame index f and then we set the current progress value (f/NF) to the current alpha --a. If f has reached either 0 of NF, we change the direction. Then the update() function gets called again on the next refresh.

(function update() { f += dir; document.body.style.setProperty('--a', (f/NF).toFixed(2)); if(!(f%NF)) dir *= -1; requestAnimationFrame(update)
})();

And that’s all for the JavaScript! We now have an animated result:

Animated gif. We animate the alpha of the gradient stops, such that the ripples go from fully opaque to fully transparent, effectively becoming gaps, while the opposite happens for the initial gaps, they go from fully transparent to fully opaque, thus becoming ripples. At any moment, the alpha of either of them is 1 minus the alpha of the other, so they complement each other. In this case, the animation is linear, the alpha changing at the same rate from start to finish.
Ripple alpha animation, linear (live demo, only works in Firefox 53+).

This is a linear animation, the alpha value --a being set to the progress f/NF. But we can change the timing function to something else, as explained in an earlier article I wrote on emulating CSS timing functions with JavaScript.

For example, if we want an ease-in kind of timing function, we set the alpha value to easeIn(f/NF) instead of just f/NF, where we have that easeIn() is:

function easeIn(k, e = 1.675) { return Math.pow(k, e)
}

The result when using an ease-in timing function can be seen in this Pen (working only in Firefox 53+). If you’re interested in how we got this function, it’s all explained in the previously linked article on timing functions.

The exact same approach works for easeOut() or easeInOut():

function easeOut(k, e = 1.675) { return 1 - Math.pow(1 - k, e)
}; function easeInOut(k) { return .5*(Math.sin((k - .5)*Math.PI) + 1)
}

Since we’re using JavaScript anyway, we can make the whole thing interactive, so that the animation only happens on click/tap, for example.

In order to do so, we add a request ID variable (rID), which is initially null, but then takes the value returned by requestAnimationFrame() in the update() function. This enables us to stop the animation with a stopAni() function whenever we want to:

 /* same as before */ let rID = null; function stopAni() { cancelAnimationFrame(rID); rID = null
}; function update() { /* same as before */ if(!(f%NF)) { stopAni(); return } rID = requestAnimationFrame(update)
};

On click, we stop any animation that may be running, reverse the animation direction dir and call the update() function:

addEventListener('click', e => { if(rID) stopAni(); dir *= -1; update()
}, false);

Since we start with the current frame index f being 0, we want to go in the positive direction, towards NF on the first click. And since we’re reversing the direction on every click, it results that the initial value for the direction must be -1 now so that it gets reversed to +1 on the first click.

The result of all the above can be seen in this interactive Pen (working only in Firefox 53+).

We could also use a different alpha variable for each stop, just like we did in the case of the rays:

$m: repeating-conic-gradient(#000 .5*$pc, transparent 0% $pc) exclude, repeating-radial-gradient(closest-side, rgba(#000, var(--a0)), rgba(#000, var(--a1)) .5*$pr, rgba(#000, var(--a2)) 0, rgba(#000, var(--a3)) $pr);

In the JavaScript, we have the ease-in and ease-out timing functions:

const TFN = { 'ease-in': function(k, e = 1.675) { return Math.pow(k, e) }, 'ease-out': function(k, e = 1.675) { return 1 - Math.pow(1 - k, e) }
};

In the update() function, the only difference from the first animated demo is that we don’t change the value of just one CSS variable—we now have four to take care of: --a0, --a1, --a2, --a3. We do this within a loop, using the ease-in function for the ones at even indices and the ease-out function for the others. For the first two, the progress is given by f/NF, while for the last two, the progress is given by 1 - f/NF. Putting all of this into one formula, we have:

(function update() { f += dir; for(var i = 0; i < 4; i++) { let j = ~~(i/2); document.body.style.setProperty( `--a${i}`, TFN[i%2 ? 'ease-out' : 'ease-in'](j + Math.pow(-1, j)*f/NF).toFixed(2) ) } if(!(f%NF)) dir *= -1; requestAnimationFrame(update)
})();

The result can be seen below:

Animated gif. This time, the alpha of each and every stop (start and end of ripple, start and end of gap) is animated independently via its own CSS variable. The alphas at the start and end of the ripple both go from 1 to 0, but using different timing functions. The alphas at the start and end of the gap both go from 0 to 1, but, again, using different timing functions.
Multiple ripple alpha animations (live demo, only works in Firefox 53+).

Just like for conic gradients, we can also animate the stop position between the opaque and the transparent part of the masking radial gradient. To do so, we use a CSS variable --p for the progress of this stop position:

$m: repeating-conic-gradient(#000 .5*$pc, transparent 0% $pc) exclude, repeating-radial-gradient(closest-side, #000, #000 calc(var(--p)*#{$pr}), transparent 0, transparent $pr);

The JavaScript is almost identical to that for the first alpha animation, except we don’t update an alpha --a variable, but a stop progress --p variable and we use an ease-in-out kind of function:

/* same as before */ function easeInOut(k) { return .5*(Math.sin((k - .5)*Math.PI) + 1)
}; (function update() { f += dir; document.body.style.setProperty('--p', easeInOut(f/NF).toFixed(2)); /* same as before */
})();
Animated gif. The stop position between the ripple an the gap animates from 0 (when the ripple is basically reduced to nothing) to the whole percentage $pr allocated for a ripple and the gap following it (which basically means we don't have a gap anymore) and then back to 0 again.
Alternating ripple size animation (live demo, only works in Firefox 53+).

We can make the effect more interesting if we add a transparent strip before the opaque one and we also animate the progress of the stop position --p0 where we go from this transparent strip to the opaque one:

$m: repeating-conic-gradient(#000 .5*$pc, transparent 0% $pc) exclude, repeating-radial-gradient(closest-side, transparent, transparent calc(var(--p0)*#{$pr}), #000, #000 calc(var(--p1)*#{$pr}), transparent 0, transparent $pr);

In the JavaScript, we now need to animate two CSS variables: --p0 and --p1. We use an ease-in timing function for the first and an ease-out for the second one. We also don’t reverse the animation direction anymore:

const NF = 120, TFN = { 'ease-in': function(k, e = 1.675) { return Math.pow(k, e) }, 'ease-out': function(k, e = 1.675) { return 1 - Math.pow(1 - k, e) } }; let f = 0; (function update() { f = (f + 1)%NF; for(var i = 0; i < 2; i++) document.body.style.setProperty(`--p${i}`, TFN[i ? 'ease-out' : 'ease-in'](f/NF); requestAnimationFrame(update)
})();

This gives us a pretty interesting result:

Animated gif. We now have one extra transparent circular strip before the opaque and transparent ones we previously had. Initially, both the start and end stop positions of this first strip and the following opaque one are 0, so they're both reduced to nothing and the whole space is occupied by the last transparent strip. The end stop positions of both strips then animate from 0 to the whole percentage $pr allocated for one repetition of our radial gradient, but with different timing functions. The end stop position of the first opaque strip animates slowly at first and faster towards the end (ease-in), while the end stop position of the opaque strip animates faster at first and slower towards the end (ease-out). This makes the opaque strip in the middle grow from nothing at first as its end stop position increases faster than that of the first transparent strip (which determines the start stop position of the opaque strip), then shrink back to nothing as its end stop position ends up being equal to $pr, just like the end stop position of the first transparent strip. The whole cycle then repeats itself.
Double ripple size animation (live demo, only works in Firefox 53+).

The post 1 HTML Element + 5 CSS Properties = Magic! appeared first on CSS-Tricks.

Theming With Variables: Globals and Locals

Cliff Pyles contributed to this post.

Setting CSS variables to theme a design system can be tricky: if they are too scoped, the system will lose consistency. If they are too global, you lose granularity.

Maybe we can fix both issues. I’d like to try to boil design system variables down to two types: Global and Component variables. Global variables will give us consistency across components. Component variables will give us granularity and isolation. Let me show you how to do it by taking a fairly simple component as an example.

Heads up, I’ll be using CSS variables for this article but the concept applies to preprocessor variables as well.

Global-scoped variables

System-wide variables are general concepts defined to keep consistency across your components.

Starting with an .alert component as an example, let’s say we want to keep consistency for all of our spaces on margins and paddings. We can first define global spacers:

:root { --spacer-sm: .5rem; --spacer-md: 1rem; --spacer-lg: 2rem;
}

And then use on our components:

/* Defines the btn component */
.btn { padding: var(--spacer-sm) var(--spacer-md);
} /* Defines the alert component */
.alert { padding: var(--spacer-sm) var(--spacer-md);
}

The main benefits of this approach are:

  • It generates a single source of truth for spacers, and a single point for the author using our system to customize it.
  • It achieves consistency since every component follows the same spacing.
  • It produces a common point of reference for designers and developers to work from. As long as the designers follow the same spacing restrictions, the translation to code is seamless.

But it also presents a few problems:

  • The system loses modularity by generating a dependency tree. Since components depend on global variables, they are no longer isolated.
  • It doesn’t allow authors to customize a single component without overwriting the CSS. For example, to change the padding of the alert without generating a system wide shift, they’d have to overwrite the alert component:
.alert { padding-left: 1rem; padding-right: 1rem;
}

Chris Coyier explains the idea of theming with global variables using custom elements in this article.

Component-scoped variables

As Robin Rendle explain in his article, component variables are scoped to each module. If we generate the alert with these variables, we’d get:

.alert { --alert-color: #222; color: var(--alert-color); border-color: var(--alert-color);
}

The main advantages are:

  • It creates a modular system with isolated components.
  • Authors get granular control over components without overwriting them. They’d just redefine the value of the variable.

There is no way to keep consistency across components or to make a system wide change following this method.

Let’s see how we can get the best of both worlds!

The two-tier theming system

The solution is a two-layer theming system where global variables always inform component variables. Each one of those layers follow a set of very specific rules.

First tier: Global variables

The main reason to have global variables is to maintain consistency, and they adhere to these rules:

  • They are prefixed with the word global and follow the formula --global--concept--modifier--state--PropertyCamelCase
    • a concept is something like a spacer or main-title
    • a state is something like hover, or expanded
    • a modifier is something like sm, or lg
    • and a PropertyCamelCase is something like BackgroundColor or FontSize
  • They are concepts, never tied to an element or component
    • this is wrong: --global-h1-font-size
    • this is right: --global--main-title--FontSize

For example, a global variable setup would look like:

:root { /* --global--concept--size */ --global--spacer--sm: .5rem; --global--spacer--md: 1rem; --global--spacer--lg: 2rem; /* --global--concept--PropertyCamelCase */ --global--main-title--FontSize: 2rem; --global--secondary-title--FontSize: 1.8rem; --global--body--FontSize: 1rem; /* --global--state--PropertyCamelCase */ --global--hover--BackgroundColor: #ccc;
}

Second tier: Component variables

The second layer is scoped to theme-able component properties and follow these rules:

  • Assuming we are writing BEM, they follow this formula: --block__element--modifier--state--PropertyCamelCase
    • The block__element--modifier the selector name is something like alert__actions or alert--primary
    • a state is something like hover or active
    • and if you are not writing BEM class names the same principles apply, just replace the block__element--modifier with your classname
  • The value of component scoped variables is always defined by a global variable
  • A component variable always has a default value as a fallback in case the component doesn’t have the dependency on the global variables

For example:

.alert { /* Component scoped variables are always defined by global variables */ --alert--Padding: var(--global--spacer--md); --alert--primary--BackgroundColor: var(--global--primary-color); --alert__title--FontSize: var(--global--secondary-title--FontSize); /* --block--PropertyCamelCase */ padding: var(--alert--Padding, 1rem); /* Sets the fallback to 1rem. */
} /* --block--state--PropertyCamelCase */
.alert--primary { background-color: var(--alert--primary--BackgroundColor, #ccc);
} /* --block__element--PropertyCamelCase */
.alert__title { font-size: var(--alert__title--FontSize, 1.8rem);
}

You’ll notice that we are defining locally-scoped variables with global variables. This is key for the system to work since it allows authors to theme the system as a whole. For example, if they want to change the primary color across all components they just need to redefine --global--primary-color.

On the other hand each component variable has a default value so a component can stand on its own, it doesn’t depend on anything and authors can use it in isolation.

This setup allows for consistency across components, it generates a common language between designers and developers since we can set the same global variables in Sketch as bumpers for designers, and it gives granular control to authors.

Why does this system work?

In an ideal world, we as creators of a design system, expect “authors” or users of our system to implement it without modifications, but of course, the world is not ideal and that never happens.

If we allow authors to easily theme the system without having to overwrite CSS, we’ll not only make their lives easier but also reduce the risk of breaking modules. At the end of the day, a maintainable system is a good system.

The two-tier theming system generates modular and isolated components where authors have the possibility to customize them at a global and at a component level. For example:

:root { /* Changes the secondary title size across the system */ --global--secondary-title--FontSize: 2rem;
} .alert { /* Changes the padding on the alert only */ --alert--Padding: 3rem;
}

What values should became variables?

CSS variables open windows to the code. The more we allow authors in, the more vulnerable the system is to implementation issues.

To keep consistency, set global variables for everything except layout values; you wouldn’t want authors to break the layout. And as a general rule, I’d recommend allowing access to components for everything you are willing to give support.

For the next version of PatternFly, an open source design system I work on, we’ll allow customization for almost everything that’s not layout related: colors, spacer, typography treatment, shadows, etc.

Putting everything together

To show this concept in action I’ve created a CodePen project:

Global variables are nestled in _global-variables.scss. They are the base to keep consistency across the system and will allow the author to make global changes.

There are two components: alert and button. They are isolated and modular entities with scoped variables that allow authors to fine tune components.

Remember that authors will use our system as a dependency in their project. By letting them modify the look and feel of the system through CSS variables, we are creating a solid code base that’s easier to maintain for the creators of the system and better to implement, modify, and upgrade to authors using the system.

For example, if an author wants to:

  • change the primary color to pink across the system;
  • change the danger color to orange just on the buttons;
  • and change the padding left to 2.3rem only on the alert…

…then this is how it’s done:

:root { // Changes the primary color on both the alert and the button --global--primary--Color: hotpink;
} .button { // Changes the danger color on the button only without affecting the alert --button--danger--BackgroundColor: orange; --button--danger--hover--BorderColor: darkgoldenrod;
} .alert { // Changes the padding left on the alert only without affecting the button --alert--PaddingLeft: 2.3rem;
}

The design system code base is intact and it’s just a better dependency to have.

I am aware that this is just one way to do it and I am sure there are other ways to successfully set up variables on a system. Please let me know what you think on the comments or send me a tweet. I’d love to hear about what you are doing and learn from it.

The post Theming With Variables: Globals and Locals appeared first on CSS-Tricks.

What Houdini Means for Animating Transforms

I’ve been playing with CSS transforms for over five years and one thing that has always bugged me was that I couldn’t animate the components of a transform chain individually. This article is going to explain the problem, the old workaround, the new magic Houdini solution and, finally, will offer you a feast of eye candy through better looking examples than those used to illustrate concepts.

The Problem

In order to better understand the issue at hand, let’s consider the example of a box we move horizontally across the screen. This means one div as far as the HTML goes:

<div class="box"></div>

The CSS is also pretty straightforward. We give this box dimensions, a background and position it in the middle horizontally with a margin.

$d: 4em; .box { margin: .25*$d auto; width: $d; height: $d; background: #f90;
}

See the Pen by thebabydino (@thebabydino) on CodePen.

Next, with the help of a translation along the x axis, we move it by half a viewport (50vw) to the left (in the negative direction of the x axis, the positive one being towards the right):

transform: translate(-50vw);

See the Pen by thebabydino (@thebabydino) on CodePen.

Now the left half of the box is outside the screen. Decreasing the absolute amount of translation by half its edge length puts it fully within the viewport while decreasing it by anything more, let’s say a full edge length (which is $d or 100%—remember that % values in translate() functions are relative to the dimensions of the element being translated), makes it not even touch the left edge of the viewport anymore.

transform: translate(calc(-1*(50vw - 100%)));

See the Pen by thebabydino (@thebabydino) on CodePen.

This is going to be our initial animation position.

We then create a set of @keyframes to move the box to the symmetrical position with respect to the initial one with no translation and reference them when setting the animation:

$t: 1.5s; .box { /* same styles as before */ animation: move $t ease-in-out infinite alternate;
} @keyframes move { to { transform: translate(calc(50vw - 100%)); }
}

This all works as expected, giving us a box that moves from left to right and back:

See the Pen by thebabydino (@thebabydino) on CodePen.

But this is a pretty boring animation, so let’s make it more interesting. Let’s say we want the box to be scaled down to a factor of .1 when it’s in the middle and have its normal size at the two ends. We could add one more keyframe:

50% { transform: scale(.1); }

The box now also scales (demo), but, since we’ve added an extra keyframe, the timing function is not applied for the whole animation anymore—just for the portions in between keyframes. This makes our translation slow in the middle (at 50%) as we now also have a keyframe there. So we need to tweak the timing function, both in the animation value and in the @keyframes. In our case, since we want to have an ease-in-out overall, we can split it into one ease-in and one ease-out.

.box { animation: move $t ease-in infinite alternate;
} @keyframes move { 50% { transform: scale(.1); animation-timing-function: ease-out; } to { transform: translate(calc(50vw - 100%)); }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

Now all works fine, but what if we wanted different timing functions for the translation and scaling? The timing functions we’ve set mean the animation is slower at the beginning, faster in the middle and then slower again at the end. What if we wanted this to apply just to the translation, but not to the scale? What if we wanted the scaling to happen fast at the beginning, when it goes from 1 towards .1, slow in the middle when it’s around .1 and then fast again at the end when it goes back to 1?

SVG illustration. Shows the timeline, highlighting the 0%, 50% and 100% keyframes. At 0%, we want the translation to start slowly, but the scaling to start fast. At 50%, we want the translation to be at its fastest, while the scaling would be at its slowest. At 100%, the translation ends slowly, while the scaling ends fast.
The animation timeline (live).

Well, it’s just not possible to set different timing functions for different transform functions in the same chain. We cannot make the translation slow and the scaling fast at the beginning or the other way around in the middle. At least, not while what we animate is the transform property and they’re part of the same transform chain.

The Old Workaround

There are of course ways of going around this issue. Traditionally, the solution has been to split the transform (and consequently, the animation) over multiple elements. This gives us the following structure:

<div class="wrap"> <div class="box"></div>
</div>

We move the width property on the wrapper. Since div elements are block elements by default, this will also determine the width of its .box child without us having to set it explicitly. We keep the height on the .box however, as the height of a child (the .box in this case) also determines the height of its parent (the wrapper in this case).

We also move up the margin, transform and animation properties. In addition to this, we switch back to an ease-in-out timing function for this animation. We also modify the move set of @keyframes to what it was initially, so that we get rid of the scale().

.wrap { margin: .25*$d calc(50% - #{.5*$d}); width: $d; transform: translate(calc(-1*(50vw - 100%))); animation: move $t ease-in-out infinite alternate;
} @keyframes move { to { transform: translate(calc(50vw - 100%)); }
}

We create another set of @keyframes which we use for the actual .box element. This is an alternating animation of half the duration of the one producing the oscillatory motion.

.box { height: $d; background: #f90; animation: size .5*$t ease-out infinite alternate;
} @keyframes size { to { transform: scale(.1); } }

We now have the result we wanted:

See the Pen by thebabydino (@thebabydino) on CodePen.

This is a solid workaround that doesn’t add too much extra code, not to mention the fact that, in this particular case, we don’t really need two elements, we could do with just one and one of its pseudo-elements. But if our transform chain gets longer, we have no choice but to add extra elements. And, in 2018, we can do better than that!

The Houdini Solution

Some of you may already know that CSS variables are not animatable (and I guess anyone who didn’t just found out). If we try to use them in an animation, they just flip from one value to the other when half the time in between has elapsed.

Consider the initial example of the oscillating box (no scaling involved). Let’s say we try to animate it using a custom property --x:

.box { /* same styles as before */ transform: translate(var(--x, calc(-1*(50vw - #{$d})))); animation: move $t ease-in-out infinite alternate
} @keyframes move { to { --x: calc(50vw - #{$d}) } }

Sadly, this just results in a flip at 50%, the official reason being that browsers cannot know the type of the custom property (which doesn’t make sense to me, but I guess that doesn’t really matter).

See the Pen by thebabydino (@thebabydino) on CodePen.

But we can forget about all of this because now Houdini has entered the picture and we can register such custom properties so that we explicitly give them a type (the syntax).

For more info on this, check out the talk and slides by Serg Hospodarets.

CSS.registerProperty({ name: '--x', syntax: '<length>', initialValue: 0
});

We’ve set the initialValue to 0, because we have to set it to something and that something has to be a computationally independent value—that is, it cannot depend on anything we can set or change in the CSS and, given the initial and final translation values depend on the box dimensions, which we set in the CSS, calc(-1*(50vw - 100%)) is not valid here. It doesn’t even work to set --x to calc(-1*(50vw - 100%)), we need to use calc(-1*(50vw - #{$d})) instead.

$d: 4em;
$t: 1.5s; .box { margin: .25*$d auto; width: $d; height: $d; --x: calc(-1*(50vw - #{$d})); transform: translate(var(--x)); background: #f90; animation: move $t ease-in-out infinite alternate;
} @keyframes move { to { --x: calc(50vw - #{$d}); } }
Animated gif. Shows a square box oscillating horizontally from left to right and back. The motion is slow at the left and right ends and faster in the middle.
The simple oscillating box we get using the new method (live demo, needs Houdini support).

For now, this only works in Blink browsers behind the Experimental Web Platform features flag. This can be enabled from chrome://flags (or, if you’re using Opera, opera://flags):

Screenshot showing the Experimental Web Platform features flag being enabled in Chrome.
The Experimental Web Platform features flag enabled in Chrome.

In all other browsers, we still see the flip at 50%.

Applying this to our oscillating and scaling demo means we introduce two custom properties we register and animate—one is the translation amount along the x axis (--x) and the other one is the uniform scaling factor (--f).

CSS.registerProperty({ /* same as before */ }); CSS.registerProperty({ name: '--f', syntax: '<number>', initialValue: 1
});

The relevant CSS is as follows:

.box { --x: calc(-1*(50vw - #{$d})); transform: translate(var(--x)) scale(var(--f)); animation: move $t ease-in-out infinite alternate, size .5*$t ease-out infinite alternate;
} @keyframes move { to { --x: calc(50vw - #{$d}); } } @keyframes size { to { --f: .1 } }
Animated gif. Shows the same oscillating box from before now also scaling down to 10% when it's right in the middle. The scaling is fast at the beginning and the end and slow in the middle.
The oscillating and scaling with the new method (live demo, needs Houdini support).

Better Looking Stuff

A simple oscillating and scaling square isn’t the most exciting thing though, so let’s see nicer demos!

Screenshots of the two demos we dissect here. Left: a rotating wavy rainbow grid of cubes. Right: bouncing square.
More interesting examples. Left: rotating wavy grid of cubes. Right: bouncing square.

The 3D version

Going from 2D to 3D, the square becomes a cube and, since just one cube isn’t interesting enough, let’s have a whole grid of them!

We consider the body to be our scene. In this scene, we have a 3D assembly of cubes (.a3d). These cubes are distributed on a grid of nr rows and nc columns:

- var nr = 13, nc = 13;
- var n = nr*nc; .a3d while n-- .cube - var n6hedron= 6; // cube always has 6 faces while n6hedron-- .cube__face

The first thing we do is a few basic styles to create a scene with a perspective, put the whole assembly in the middle and put each cube face into its place. We won’t be going into the details of how to build a CSS cube because I’ve already dedicated a very detailed article to this topic, so if you need a recap, check that one out!

The result so far can be seen below – all the cubes stacked up in the middle of the scene:

Screenshot. Shows all cubes (as wireframes) in the same position in the middle of the scene, making it look as if there's only one wireframe.
All the cubes stacked up in the middle (live demo).

For all these cubes, their front half is in front of the plane of the screen and their back half is behind the plane of the screen. In the plane of the screen, we have a square section of our cube. This square is identical to the ones representing the cube faces.

See the Pen by thebabydino (@thebabydino) on CodePen.

Next, we set the column (--i) and row (--j) indices on groups of cubes. Initially, we set both these indices to 0 for all cubes.

.cube { --i: 0; --j: 0;
}

Since we have a number of cubes equal to the number of columns (nc) on every row, we then set the row index to 1 for all cubes after the first nc ones. Then, for all cubes after the first 2*nc ones, we set the row index to 2. And so on, until we’ve covered all nr rows:

style | .cube:nth-child(n + #{1*nc + 1}) { --j: 1 } | .cube:nth-child(n + #{2*nc + 1}) { --j: 2 } //- and so on | .cube:nth-child(n + #{(nr - 1)*nc + 1}) { --j: #{nr - 1} }

We can compact this in a loop:

style - for(var i = 1; i < nr; i++) { | .cube:nth-child(n + #{i*nc + 1}) { --j: #{i} } -}

Afterwards, we move on to setting the column indices. For the columns, we always need to skip a number of cubes equal to nc - 1 before we encounter another cube with the same index. So, for every cube, the nc-th cube after it is going to have the same index and we’re going to have nc such groups of cubes.

(We only need to set the index to the last nc - 1, because all cubes have the column index set to 0 initially, so we can skip the first group containing the cubes for which the column index is 0 – no need to set --i again to the same value it already has.)

style | .cube:nth-child(#{nc}n + 2) { --i: 1 } | .cube:nth-child(#{nc}n + 3) { --i: 2 } //- and so on | .cube:nth-child(#{nc}n + #{nc}) { --i: #{nc - 1} }

This, too, can be compacted in a loop:

style - for(var i = 1; i < nc; i++) { | .cube:nth-child(#{nc}n + #{i + 1}) { --i: #{i} } -}

Now that we have all the row and column indices set, we can distribute these cubes on a 2D grid in the plane of the screen using a 2D translate() transform, according to the illustration below, where each cube is represented by its square section in the plane of the screen and the distances are measured in between transform-origin points (which are, by default, at 50% 50% 0, so dead in the middle of the square cube sections from the plane of the screen):

SVG illustration. Shows how to create a basic grid of square, vertical cube sections with nc columns and nr rows starting from the position of the top left item. The top left item is on the first column (of index <code>0</code>) and on the first row (of index <code>0</code>). All items on the second column (of index <code>1</code>) are offset horizontally by and edge length. All items on the third column (of index <code>2</code>) are offset horizontally by two edge lengths. In general, all items on the column of index <code>i</code> are offset horizontally by <code>i</code> edge lengths. All items on the last column (of index <code>nc - 1</code>) are offset horizontally by <code>nc - 1</code> edge lengths. All items on the second row (of index <code>1</code>) are offset vertically by and edge length. All items on the third row (of index <code>2</code>) are offset vertically by two edge lengths. In general, all items on the row of index <code>j</code> are offset vertically by <code>j</code> edge lengths. All items on the last row (of index <code>nr - 1</code>) are offset vertically by <code>nr - 1</code> edge lengths.”/><figcaption>How to create a basic grid starting from the position of the top left item (live).</figcaption></figure>
<pre rel=/* $l is the cube edge length */ .cube { /* same as before */ --x: calc(var(--i)*#{$l}); --y: calc(var(--j)*#{$l}); transform: translate(var(--x), var(--y)); }

This gives us a grid, but it’s not in the middle of the screen.

Screenshot. Shows the grid with nc columns and nr rows, with cubes repersented as wireframes. The midpoint of the top left cube of the rectangular grid is dead in the middle of the screen..
The grid, having the midpoint of the top left cube in the middle of the screen (live demo).

Right now, it’s the central point of the top left cube that’s in the middle of the screen, as highlighted in the demo above. What we want is for the grid to be in the middle, meaning that we need to shift all cubes left and up (in the negative direction of both the x and y axes) by the horizontal and vertical differences between half the grid dimensions (calc(.5*var(--nc)*#{$l}) and calc(.5*var(--nr)*#{$l}), respectively) and the distances between the top left corner of the grid and the midpoint of the top left cube’s vertical cross-section in the plane of the screen (these distances are each half the cube edge, or .5*$l).

The difference between the position of the grid midpoint and the top left item midpoint (live).

Subtracting these differences from the previous amounts, our code becomes:

.cube { /* same as before */ --x: calc(var(--i)*#{$l} - (.5*var(--nc)*#{$l} - .5*#{$l})); --y: calc(var(--j)*#{$l} - (.5*var(--nr)*#{$l} - .5*#{$l}));
}

Or even better:

.cube { /* same as before */ --x: calc((var(--i) - .5*(var(--nc) - 1))*#{$l})); --y: calc((var(--j) - .5*(var(--nr) - 1))*#{$l}));
}

We also need to make sure we set the --nc and --nr custom properties:

- var nr = 13, nc = 13;
- var n = nr*nc; //- same as before
.a3d(style=`--nc: ${nc}; --nr: ${nr}`) //- same as before

This gives us a grid that’s in the middle of the viewport:

Screenshot. Shows a grid of cube wireframes right in the middle.
The grid is now in the middle (live).

We’ve also made the cube edge length $l smaller so that the grid fits within the viewport.

Alternatively, we can go for a CSS variable --l instead so that we can control the edge length depending on the number of columns and rows. The first step here is setting the maximum of the two to a --nmax variable:

- var nr = 13, nc = 13;
- var n = nr*nc; //- same as before
.a3d(style=`--nc: ${nc}; --nr: ${nr}; --max: ${Math.max(nc, nr)}`) //- same as before

Then, we set the edge length (--l) to something like 80% (completely arbitrary value) of the minimum viewport dimension over this maximum (--max):

.cube { /* same as before */ --l: calc(80vmin/var(--max));
}

Finally, we update the cube and face transforms, the face dimensions and margin to use --l instead of $l:

.cube { /* same as before */ --l: calc(80vmin/var(--max)); --x: calc((var(--i) - .5*(var(--nc) - 1))*var(--l)); --y: calc((var(--j) - .5*(var(--nr) - 1))*var(--l)); &__face { /* same as before */ margin: calc(-.5*var(--l)); width: var(--l); height: var(--l); transform: rotate3d(var(--i), var(--j), 0, calc(var(--m, 1)*#{$ba4gon})) translatez(calc(.5*var(--l))); }
}

Now we have a nice responsive grid!

Animated gif. Shows the previously created grid scaling with the viewport.
The grid is now in the middle and responsive such that it always fits within the viewport (live).

But it’s an ugly one, so let’s turn it into a pretty rainbow by making the color of each cube depend on its column index (--i):

.cube { /* same as before */ color: hsl(calc(var(--i)*360/var(--nc)), 65%, 65%);
}
Screenshot. The assembly wireframe has now a rainbow look, with every column of cubes having a different hue.
The rainbow grid (live demo).

We’ve also made the scene background dark so that we have better contrast with the now lighter cube edges.

To spice things up even further, we add a row rotation around the y axis depending on the row index (--j):

.cube { /* same as before */ transform: rotateY(calc(var(--j)*90deg/var(--nr))) translate(var(--x), var(--y));
}
Screenshot. The assembly wireframe now appears twisted, with every row being rotated at a different angle, increasing from top to bottom.
The twisted grid (live demo).

We’ve also decreased the cube edge length --l and increased the perspective value in order to allow this twisted grid to fit in.

Now comes the fun part! For every cube, we animate its position back and forth along the z axis by half the grid width (we make the translate() a translate3d() and use an additional custom property --z that goes between calc(.5*var(--nc)*var(--l)) and calc(-.5*var(--nc)*var(--l))) and its size (via a uniform scale3d() of factor --f that goes between 1 and .1). This is pretty much the same thing we did for the square in our original example, except the motion now happens along the z axis, not along the x axis and the scaling happens in 3D, not just in 2D.

$t: 1s; .cube { /* same as before */ --z: calc(var(--m)*.5*var(--nc)*var(--l)); transform: rotateY(calc(var(--j)*90deg/var(--nr))) translate3d(var(--x), var(--y), var(--z)) scale3d(var(--f), var(--f), var(--f)); animation: a $t ease-in-out infinite alternate; animation-name: move, zoom; animation-duration: $t, .5*$t;
} @keyframes move { to { --m: -1 } } @keyframes zoom { to { --f: .1 } }

This doesn’t do anything until we register the multiplier --m and the scaling factor --f to give them a type and an initial value:

CSS.registerProperty({ name: '--m', syntax: '<number>', initialValue: 1
}); CSS.registerProperty({ name: '--f', syntax: '<number>', initialValue: 1
});
Animated gif. Every cube now moves back and forth along its own z axis (post row rotation), between half a grid width behind its xOy plane and half a grid width in front of its xOy plane. Each cube also scales along all three axes, going from its initial size to a tenth of it along each axis and then back to its initial size.
The animated grid (live demo, needs Houdini support).

At this point, all cubes animate at the same time. To make things more interesting, we add a delay that depends on both the column and row index:

animation-delay: calc((var(--i) + var(--j))*#{-2*$t}/(var(--nc) + var(--nr)));
Screenshot
The waving grid effect (live).

The final touch is to add a rotation on the 3D assembly:

.a3d { top: 50%; left: 50%; animation: ry 8s linear infinite;
} @keyframes ry { to { transform: rotateY(1turn); } }

We also make the faces opaque by giving them a black background and we have the final result:

Animated gif. Now the cube faces are opaque (we've given them a black background) whole assembly rotates around its y axis, making the animation more interesting.
The final result (live demo, needs Houdini support).

The performance for this is pretty bad, as it can be seen from the GIF recording above, but it’s still interesting to see how far we can push things.

Hopping Square

I came across the original in a comment to another article and, as soon as I saw the code, I thought it was the perfect candidate for a makeover using some Houdini magic!

Let’s start by understanding what is happening in the original code.

In the HTML, we have nine divs.


<div class="frame"> <div class="center"> <div class="down"> <div class="up"> <div class="squeeze"> <div class="rotate-in"> <div class="rotate-out"> <div class="square"></div> </div> </div> </div> </div> </div> <div class="shadow"></div> </div>
</div>

Now, this animation is a lot more complex than anything I could ever come up with, but, even so, nine elements seems to be overkill. So let’s take a look at the CSS, see what they’re each used for and see how much we can simplify the code in preparation for switching to the Houdini-powered solution.

Let’s start with the animated elements. The .down and .up elements each have an animation related to moving the square vertically:

/* original */
.down { position: relative; animation: down $duration ease-in infinite both; .up { animation: up $duration ease-in-out infinite both; /* the rest */ }
} @keyframes down { 0% { transform: translateY(-100px); } 20%, 100% { transform: translateY(0); }
} @keyframes up { 0%, 75% { transform: translateY(0); } 100% { transform: translateY(-100px); }
}

With @keyframes and animations on both elements having the same duration, we can pull off a make-one-out-of-two trick.

In the case of the first set of @keyframes, all the action (going from -100px to 0) happens in the [0%, 20%] interval, while, in the case of the second one, all the action (going from 0 to -100px) happens in the [75%, 100%] interval. These two intervals don’t intersect. Because of this and because both animations have the same duration we can add up the translation values at each keyframe.

  • at 0%, we have -100px from the first set of @keyframes and 0 from the second, which gives us -100px
  • at 20%, we have 0 from the first set of @keyframes and 0 from the second (as we have 0 for any frame from 0% to 75%), which gives us 0
  • at 75%, we have 0 from the first set of @keyframes (as we have 0 for any frame from 20% to 100%) and 0 from the second, which gives us 0
  • at 100%, we have 0 from the first set of @keyframes and -100px from the second, which gives us -100px

Our new code is as follows. We have removed the animation-fill-mode from the shorthand as it doesn’t do anything in this case since our animation loops infinitely, has a non-zero duration and no delay:

/* new */
.jump { position: relative; transform: translateY(-100px); animation: jump $duration ease-in infinite; /* the rest */
} @keyframes jump { 20%, 75% { transform: translateY(0); animation-timing-function: ease-in-out; }
}

Note that we have different timing functions for the two animations, so we need to switch between them in the @keyframes. We still have the same effect, but we got rid of one element and one set of @keyframes.

Next, we do the same thing for the .rotate-in and .rotate-out elements and their @keyframes:

/* original */
.rotate-in { animation: rotate-in $duration ease-out infinite both; .rotate-out { animation: rotate-out $duration ease-in infinite both; }
} @keyframes rotate-in { 0% { transform: rotate(-135deg); } 20%, 100% { transform: rotate(0deg); }
} @keyframes rotate-out { 0%, 80% { transform: rotate(0); } 100% { transform: rotate(135deg); }
}

In a similar manner to the previous case, we add up the rotation values for each keyframe.

  • at 0%, we have -135deg from the first set of @keyframes and 0deg from the second, which gives us -135deg
  • at 20%, we have 0deg from the first set of @keyframes and 0deg from the second (as we have 0deg for any frame from 0% to 80%), which gives us 0deg
  • at 80%, we have 0deg from the first set of @keyframes (as we have 0deg for any frame from 20% to 100%) and 0deg from the second, which gives us 0deg
  • at 100%, we have 0deg from the first set of @keyframes and 135deg from the second, which gives us 135deg

This means we can compact things to:

/* new */
.rotate { transform: rotate(-135deg); animation: rotate $duration ease-out infinite;
} @keyframes rotate { 20%, 80% { transform: rotate(0deg); animation-timing-function: ease-in; } 100% { transform: rotate(135deg); }
}

We only have one element with a scaling transform that distorts our white square:

/* original */
.squeeze { transform-origin: 50% 100%; animation: squeeze $duration $easing infinite both;
} @keyframes squeeze { 0%, 4% { transform: scale(1); } 45% { transform: scale(1.8, 0.4); } 100% { transform: scale(1); }
}

There’s not really much we can do here in terms of compacting the code, save for removing the animation-fill-mode and grouping the 100% keyframe with the 0% and 4% ones:

/* new */
.squeeze { transform-origin: 50% 100%; animation: squeeze $duration $easing infinite;
} @keyframes squeeze { 0%, 4%, 100% { transform: scale(1); } 45% { transform: scale(1.8, .4); }
}

The innermost element (.square) is only used to display the white box and has no transform set on it.

 /* original */
.square { width: 100px; height: 100px; background: #fff;
}

This means we can get rid of it if we move its styles to its parent element.

/* new */
$d: 6.25em; .rotate { width: $d; height: $d; transform: rotate(-135deg); background: #fff; animation: rotate $duration ease-out infinite;
}

We got rid of three elements so far and our structure has become:

.frame .center .jump .squeeze .rotate .shadow

The outermost element (.frame) serves as a scene or container. This is the big blue square.

/* original */
.frame { position: absolute; top: 50%; left: 50%; width: 400px; height: 400px; margin-top: -200px; margin-left: -200px; border-radius: 2px; box-shadow: 1px 2px 10px 0px rgba(0,0,0,0.2); overflow: hidden; background: #3498db; color: #fff; font-family: 'Open Sans', Helvetica, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
}

There’s no text in this demo, so we can get rid of the text-related properties. We can also get rid of the color property since, not only do we not have text anywhere in this demo, but we’re also not using this for any borders, shadows, backgrounds (via currentColor) and so on.

We can also avoid taking this containing element out of the document flow by using a flexbox layout on the body. This also eliminates the offsets and the margin properties.

/* new */
$s: 4*$d; body { display: flex; align-items: center; justify-content: center; height: 100vh;
} .frame { overflow: hidden; position: relative; width: $s; height: $s; border-radius: 2px; box-shadow: 1px 2px 10px rgba(#000, .2); background: #3498db;
}

We’ve also tied the dimensions of this element to those of the hopping square.

The .center element is only used for positioning its direct children (.jump and .shadow), so we can take it out altogether and use the offsets on it directly on these children.

We use absolute positioning on all .frame descendants. This makes the .jump and .squeeze elements 0x0 boxes, so we tweak the transform-origin for the squeezing transform (100% of 0 is always 0, but the value we want is half the square edge length .5*$d). We also set a margin of minus half the square edge length (-.5*$d) on the .rotate element (to compensate for the translate(-50%, -50%) we had on the removed .center element).

/* new */
.frame * { position: absolute, } .jump { top: $top; left: $left; /* same as before */
} .squeeze { transform-origin: 50% .5*$d; /* same as before */
} .rotate { margin: -.5*$d; /* same as before */
}

Finally, let’s take a look at the .shadow element.

/* original */
.shadow { position: absolute; z-index: -1; bottom: -2px; left: -4px; right: -4px; height: 2px; border-radius: 50%; background: rgba(0,0,0,0.2); box-shadow: 0 0 0px 8px rgba(0,0,0,0.2); animation: shadow $duration ease-in-out infinite both;
} @keyframes shadow { 0%, 100% { transform: scaleX(.5); } 45%, 50% { transform: scaleX(1.8); }
}

We’re of course removing the position since we’ve already set that for all descendants of the .frame. We can also get rid of the z-index if we move the .shadow before the .jump element in the DOM.

Next, we have the offsets. The midpoint of the shadow is offset by $left (just like the .jump element) horizontally and by $top plus half a square edge length (.5*$d) vertically.

We see a height that’s set to 2px. Along the other axis, the width computes to the square’s edge length ($d) plus 4px from the left and 4px from the right. That’s plus 8px in total. But one thing we notice is that the box-shadow with an 8px spread and no blur is just an extension of the background. So we can just increase the dimensions of the our element by twice the spread along both axes and get rid of the box-shadow altogether.

Just like in the case of the other elements, we also get rid of the animation-fill-mode from the animation shorthand:

/* new */
.shadow { margin: .5*($d - $sh-h) (-.5*$sh-w); width: $sh-w; height: $sh-h; border-radius: 50%; transform: scaleX(.5); background: rgba(#000, .2); animation: shadow $duration ease-in-out infinite;
} @keyframes shadow { 45%, 50% { transform: scaleX(1.8); }
}

We’ve now reduced the code in the original demo by about 40% while still getting the same result.

See the Pen by thebabydino (@thebabydino) on CodePen.

Our next step is to merge the .jump, .squeeze and rotate components into one, so that we go from three elements to a single one. Just as a reminder, the relevant styles we have at this point are:

.jump { transform: translateY(-100px); animation: jump $duration ease-in infinite;
} .squeeze { transform-origin: 50% .5*$d; animation: squeeze $duration $easing infinite;
} .rotate { transform: rotate(-135deg); animation: rotate $duration ease-out infinite;
} @keyframes jump { 20%, 75% { transform: translateY(0); animation-timing-function: ease-in-out; }
} @keyframes squeeze { 0%, 4%, 100% { transform: scale(1); } 45% { transform: scale(1.8, .4); }
} @keyframes rotate { 20%, 80% { transform: rotate(0deg); animation-timing-function: ease-in; } 100% { transform: rotate(135deg); }
}

The only problem here is that the scaling transform has a transform-origin that’s different from the default 50% 50%. Fortunately, we can go around that.

Any transform with a transform-origin different from the default is equivalent to a transform chain with default transform-origin that first translates the element such that its default transform-origin point (the 50% 50% point in the case of HTML elements and the 0 0 point of the viewBox in the case of SVG elements) goes to the desired transform-origin, applies the actual transformation we want (scaling, rotation, shearing, a combination of these… doesn’t matter) and then applies the reverse translation (the values for each of the axes of coordinates are multiplied by -1).

Any transform with a transform with a transform-origin different from the default is equivalent to a chain that translates the point of the default transform-origin to that of the custom one, performs the desired transform and then reverses the initial translation (live demo).

Putting this into code means that if we have any transform with transform-origin: $x1 $y1, the following two are equivalent:

/* transform on HTML element with transform-origin != default */ transform-origin: $x1 $y1;
transform: var(--transform); /* can be rotation, scaling, shearing */ /* equivalent transform chain on HTML element with default transform-origin */
transform: translate(calc(#{$x1} - 50%), calc(#{$y1} - 50%)) var(--transform) translate(calc(50% - #{$x1}), calc(50% - $y1);

In our particular case, we have the default transform-origin value along the x axis, so we only need to perform a translation along the y axis. By also replacing the hardcoded values with variables, we get the following transform chain:

transform: translateY(var(--y)) translateY(.5*$d) scale(var(--fx), var(--fy)) translateY(-.5*$d) rotate(var(--az));

We can compact this a bit by joining the first two translations:

transform: translateY(calc(var(--y) + #{.5*$d})) scale(var(--fx), var(--fy)) translateY(-.5*$d) rotate(var(--az));

We also put the three animations on the three elements into just one:

animation: jump $duration ease-in infinite, squeeze $duration $easing infinite, rotate $duration ease-out infinite;

And we modify the @keyframes so that we now animate the newly-introduced custom properties --y, --fx, --fy and --az:

@keyframes jump { 20%, 75% { --y: 0; animation-timing-function: ease-in-out; }
} @keyframes squeeze { 0%, 4%, 100% { --fx: 1; --fy: 1 } 45% { --fx: 1.8; --fy: .4 }
} @keyframes rotate { 20%, 80% { --az: 0deg; animation-timing-function: ease-in; } 100% { --az: 135deg }
}

However, this won’t work unless we register these CSS variables we have introduced and want to animate:

CSS.registerProperty({ 'name': '--y', 'syntax': '<length>', 'initialValue': '-100px'
}); CSS.registerProperty({ 'name': '--fx', 'syntax': '<number>', 'initialValue': 1
}); /* exactly the same for --fy */ CSS.registerProperty({ 'name': '--az', 'syntax': '<angle>', 'initialValue': '-135deg'
});

We now have a working demo of the method animating CSS variables. But given that our structure is now one wrapper with two children, we can reduce it further to one element and two pseudo-elements, thus getting the final version which can be seen below. It’s worth noting that this only works in Blink browsers with the Experimental Web Platform features flag enabled.

Animated gif. The square rotates in the air, falls down and gets squished against the ground, then bounces back up and the cycle repeats.
The final result (live, needs Houdini support)

The post What Houdini Means for Animating Transforms appeared first on CSS-Tricks.

Everything you need to know about CSS Variables

This is by far the biggest deep dive I’ve seen on CSS Variables posted to the web and it’s merely Chapter One of complete e-book on the topic.

Truth is, I’m still on the thick of reading through this myself, but had to stop somewhere in the middle to write this up and share it because it’s just that gosh-darned useful. For example, the post goes into great detail on three specific use cases for CSS Variables and breaks the code down to give a better understanding of what it does, in true tutorial fashion.

Scoping, inheritance, resolving multiple declarations, little gotchas—there’s plenty in here for beginners and advanced developers alike.

OK, back to reading. 🤓

Direct Link to Article — Permalink


Everything you need to know about CSS Variables is a post from CSS-Tricks