Writing Animations That Bring Your Site to Life

Web animation is one of the factors that can strongly enhance your website’s look and feel. Sadly, unlike mobile apps, there aren’t as many websites using animation to their benefit as you would think. We don’t want to count yours among those, so this article is for you and anyone else looking for ways to use animation for a better user experience! Specifically, we’re going to learn how to make web interactions delightful using CSS animations.

Here’s what we’re going to build together:

Live Demo GitHub Repo

Before we move ahead, it’s worth mentioning that I’m going to assume you have at least some familiarity with modern front-end frameworks and a basic understanding of CSS animations. If you don’t, then no fear! CSS-Tricks has a great guides on React and Vue, as well as a thorough almanac post on the CSS animation property.

Good? OK, let’s talk about why we’d want to use animation in the first place and cover some baseline information about CSS animations.

Why would we to animate anything?

We could probably do an entire post on this topic alone. Oh, wait! Sarah Drasner already did that and her points are both poignant and compelling.

But, to sum things up based on my own experiences:

  • Animations enhance the way users interact with an interface. For example, smart animations can reduce cognitive load by giving users better context between page transitions.
  • They can provide clear cues to users, like where we want them to focus attention.
  • Animations serve as another design pattern in and of themselves, helping users to get emotionally attached to and engage with the interface.
  • Another benefit of using animations is that they can create a perception that a site or app loads faster than it actually does.

A couple of house rules with animations

Have you ever bumped into a site that animates all the things? Wow, those can be jarring. So, here’s a couple of things to avoid when working with animations so our app doesn’t fall into the same boat:

  • Avoid animating CSS properties other than transform and opacity. If other properties have to be animated, like width or height, then make sure there aren’t a lot of layout changes happening at the same time. There’s actually a cost to animations and you can see exactly how much by referring to CSS Triggers.
  • Also, just because animations can create perceived performance gains, there’s actually a point of diminishing return when it comes to using them. Animating too many elements at the same time may result in decreased performance.

Now we can get our hands dirty with some code!

Let’s build a music app

We’re going to build the music app we looked at earlier, which is inspired by Aurélien Salomon’s Dribbble shot. I chose this example so that we can focus on animations, not only within a component, but also between different routes. We’ll build this app using Vue and create animations using vanilla (i.e. no framework) CSS.

Animations should go hand-in-hand with UI development. Creating UI before defining their movement is likely to cost much more time. In this case, the Dribbble shot provides that scope for us.

Let’s start with the development.

Step 1: Spin up the app locally

First things first. We need to set up a new Vue project. Again, we’re assuming some base-level understanding of Vue here, so please check out the Learning Vue guide for more info on setting up.

We need a couple of dependencies for our work, notably vue-router for transitioning between views and sass-loader so we can write in Sass and compile to CSS. Here’s a detailed tutorial on using routes and Sass can be installed by pointing the command line at the project directory and using npm install -D sass-loader node-sass.

We have what we need!

Step 2: Setting up routes

For creating routes, we’re first going to create two bare minimum components — Artists.vue and Tracks.vue. We’ll drop a new file in the src folder called router.js and add routes for these components as:

import Vue from 'vue'
import Router from 'vue-router'
import Artists from './components/Artists.vue'
import Tracks from './components/Tracks.vue' Vue.use(Router)
export default new Router({ mode: 'history', routes: [ { path: '/', name: 'artists', component: Artists }, { path: '/:id', name: 'tracks', component: Tracks } ]
})

Import router.js into the main.js and inject it to the Vue instance. Lastly, replace the content of your App.vue by <router-view/>.

Step 3: Create the components and content for the music app

We need two components that we’ll transition between with animation. Those are going to be:

  1. Artists.vue: a grid of artists
  2. Tracks.vue: An artist image with a back button

If you wanna jump ahead a bit, here are some assets to work with:

  1. Images and sample data in JSON format.
  2. Content for the components

When all is said and done, the two views will come out to something like this:

Artists.vue (left) and Tracks.vue (right)

Step 4: Animate!

Here we are, the part we’ve really wanted to get to all this time. The most important animation in the app is transitioning from Artists to Tracks when clicking on an artist. It should feel seamless where clicking on an artist image puts that image in focus while transitioning from one view into the next. This is exactly the type of animation that we rarely see in apps but can drastically reduce cognitive load for users.

To make sure we’re all on the same page, we’re going to refer to the first image in the sequence as the “previous” image and the second one as the “current” image. Getting the effect down is relatively easy as long as we know the dimensions and position of the previous image in the transition. We can animate the current image by transforming it as per previous image.

The formula that I’m using is transform: translate(x, y) scale(n), where n is equal to the size of previous image divided by the size of current image. Note that we can use a static value of n since the dimensions are fixed for all the images. For example, the image size in the Artists view is 190x190 and 240x240 in the Tracks view. Thus, we can replace n by 190/240 = 0.791. That means the transform value becomes translate(x, y) scale(0.791) in our equation.

Animating from Artists to Tracks

Next thing is to find x and y. We can get these values though click event in the Artists view as:

const {x, y} = event.target.getBoundingClientRect()

…and then send these values to the Tracks view, all while switching the route. Since we aren’t using any state management library, the two components will communicate via their parent component, which is the top level component, App.vue. In App.vue, let’s create a method that switches the route and sends the image info as params.

gotoTracks(position, artistId) { this.$router.push({ name: 'tracks', params: { id: artistId, position: position } })
}

Here’s the relevant code from the repo to reference, in case you’re interested.

Since we have received the position and ID of the image in Tracks, we have all the required data to show and animate it. We’ll first fetch artist information (specifically the name and image URL) using artist ID.

To animate the image, we need to calculate the transform value from the image’s starting position. To set the transform value, I’m using CSS custom properties, which can be done with CSS-in-JS techniques as well. Note that the image’s position that we received through props will be relative to window. Therefore we’ll have to subtract some fixed offset caused by the padding of the container <div> to even out our math.

const { x, y } = this.$route.params.position
// padding-left
const offsetLeft = 100
// padding-top
const offsetTop = 30 // Set CSS custom property value
document.documentElement.style.setProperty( '--translate', `translate(${x - offsetLeft}px, ${y - offsetTop}px) scale(0.792)`
)

We’ll use this value to create a keyframe animation to move the image:

@keyframes move-image { from { transform: var(--translate); }
}

This gets assigned to the CSS animation:

.image { animation: move-image 0.6s;
}

…and it will animate the image from this transform value to its original position on component load.

Transitioning from Artists to Tracks

We can use the same technique when going the opposite direction, Tracks to Artists. As we already have the clicked image’s position stored in the parent component, we can pass it to props for Artists as well.

Transitioning from Tracks to Artists

Step 5: Show the tracks!

It’s great that we can now move between our two views seamlessly, but the Tracks view is pretty sparse at the moment. So let’s add the track list for the selected artist.

We’ll create an empty white box and a new keyframe to slide it upwards on page load. Then we’ll add three subsections to it: Recent Tracks, Popular Tracks, and Playlist. Again, if you want to jump ahead, feel free to either reference or copy the final code from the repo.

The Tracks view with content

Recent Tracks is the row of thumbnails just below the artist image where each thumbnail includes the track name and track length below it. Since we’re covering animations here, we’ll create a scale-up animation, where the image starts invisible (opacity: 0) and a little smaller than it’s natural size (scale(0.7)), then is revealed (opacity: 1 )and scales up to its natural size (transform: none).

.track { opacity: 0; transform: scale(0.7); animation: scale-up 1s ease forwards;
} @keyframes scale-up { to { opacity: 1; transform: none; }
}

The Popular Tracks list and Playlist sit side-by-side below the Recent Tracks, where Popular tracks takes up most of the space. We can slide them up a bit on initial view with another set of keyframes:

.track { ... animation: slide-up 1.5s;
} @keyframes slide-up { from { transform: translateY(140px); }
}

To make the animation feel more natural, we’ll create a stagger effect by adding an incremental animation delay to each item.

@for $i from 1 to 5 { &:nth-child(#{$i + 1}) { animation-delay: #{$i * 0.05}s; }
}

The code above is basically looking for each child element, then adding a 0.05 second delay to each element it finds. So, for example, the first child gets a 0.05 second delay, the second child gets a 0.10 second delay and so on.

Check out how nice and natural this all looks:

Bonus: micro-interactions!

One of the fun things about working with animations is thinking through the small details because they’re what tie things together and add delight to the user experience. We call these micro-interactions and they serve a good purpose by providing visual feedback when an action is performed.

Depending on the complexity of the animations, we might need a library like anime.js or GSAP. This example is pretty straightforward, so we can accomplish everything we need by writing some CSS.

First micro-interaction: The volume icon

Let’s first get a volume icon in SVG format (Noun Project and Material Design are good sources). On click, we’ll animate-in and out its path element to show the level of volume. For this, we’ll create a method which switches its CSS class according to the volume level.

<svg @click="changeVolume"> <g :class="`level-${volumeLevel}`"> <path d="..."/> <!-- volume level 1 --> <path d="..."/> <!-- volume level 2 --> <path d="..."/> <!-- volume level 3 --> <polygon points="..."/> </g>
</svg>

Based on this class, we can show and hide certain path elements as:

path { opacity: 0; transform-origin: left; transform: translateX(-5px) scale(0.6); transition: transform 0.25s, opacity 0.2s;
} .level-1 path:first-child,
.level-2 path:first-child,
.level-2 path:nth-child(2),
.level-3 path { opacity: 1; transform: none;
}
The animated volume control

Second micro-interaction: The favorite icon

Do you like it when you click on Twitter’s heart button? That’s because it feels unique and special by the way it animates on click. We’ll make something similar but real quick. For this, we first get an SVG heart icon and add it to the the markup. Then we’ll add a bouncy animation to it that’s triggered on click.

@keyframes bounce { 0%, 100% { transform: none; } 30% { transform: scale(1.3); } 60% { transform: scale(0.9); }
}

Another fun thing we can do is add other small heart icons around it with random sizes and positions. Ideally, we’d add a few absolute-positioned HTML elements that a heart as the background. Let’s Arrange each of them as below by setting their left and bottom values.

We’ll also include a fade away effect so the icons appear to dissolve as they move upward by adding a keyframe animation on the same click event.

@keyframes float-upwards { 0%, 100% { opacity: 0; } 50% { opacity: 0.7; } 50%, 100% { transform: translate(-1px, -5px); }
}
The animated favorite button

Summing up

That’s all! I hope you find all this motivating to try animations on your own websites and projects.

While writing this, I also wanted to expand on the fundamental animation principles we glossed over earlier because I believe that they help choose animation durations, and avoid non-meaningful animations. That’s important to discuss because doing animations correctly is better than doing them at all. But this sounds like a whole another topic to be covered in a future article.

The post Writing Animations That Bring Your Site to Life appeared first on CSS-Tricks.

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.

The State of Changing Gradients with CSS Transitions and Animations

Back in 2012, Internet Explorer 10 came out and, among other things, it finally supported CSS gradients and, in addition to that, the ability to animate them with just CSS! No other browser supported this at the time, but I was hopeful for the future.

Sadly, six years have passed and nothing has changed in this department. Edge supports animating gradients with CSS, just like IE 10 did back then, but no other browser has added support for this. And while animating background-size or background-position or the opacity or rotation of a pseudo element layered on top can take us a long way in terms of achieving cool effects, these workarounds are still limited.

There are effects we cannot reproduce without adding lots of extra elements or lots of extra gradients, such as “the blinds effect” seen below.

Animated GIF showing a recording of the opening and closing blinds effect. When the blinds are closed, we only see a grey background, when the blinds start to open, we start seeing vertical orange strips (the light coming in) that grow horizontally until the blinds are fully open, so we only see an orange background. After that, the blinds start to close, so the vertical orange strips start getting narrower until they're reduced to nothing when the blinds are fully closed and we only see a grey background again. The whole cycle then repeats itself.
The blinds effect (live demo, Edge/ IE 10+ only).

In Edge, getting the above effect is achieved with a keyframe animation:

html { background: linear-gradient(90deg, #f90 0%, #444 0) 50%/ 5em; animation: blinds 1s ease-in-out infinite alternate;
} @keyframes blinds { to { background-image: linear-gradient(90deg, #f90 100%, #444 0); }
}

If that seems WET, we can DRY it up with a touch of Sass:

@function blinds($open: 0) { @return linear-gradient(90deg, #f90 $open*100%, #444 0);
} html { background: blinds() 50%/ 5em; animation: blinds 1s ease-in-out infinite alternate;
} @keyframes blinds { to { background-image: blinds(1) } }

While we’ve made the code we write and what we’ll need to edit later a lot more maintainable, we still have repetition in the compiled CSS and we’re limited by the fact that we can only animate between stops with the same unit — while animating from 0% to 100% works just fine, trying to use 0 or 0px instead of 0% results in no animation happening anymore. Not to mention that Chrome and Firefox just flip from orange to grey with no stop position animation at all!

Fortunately, these days we have an even better option: CSS variables!

Right out of the box, CSS variables are not animatable, though we can get transition (but not animation!) effects if the property we use them for is animatable. For example, when used inside a transform function, we can transition the transform the property.

Let’s consider the example of a box that gets shifted and squished when a checkbox is checked. On this box, we set a transform that depends on a factor --f which is initially 1:

.box { /* basic styles like dimensions and background */ --f: 1; transform: translate(calc((1 - var(--f))*100vw)) scalex(var(--f));
}

When the checkbox is :checked, we change the value of the CSS variable --f to .5:

:checked ~ .box { --f: .5 }

Setting a transition on the .box makes it go smoothly from one state to the other:

.box { /* same styles as before */ transition: transform .3s ease-in;
}

Note that this doesn’t really work in the current version of Edge due to this bug.

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

However, CSS gradients are background images, which are only animatable in Edge and IE 10+. So, while we can make things easier for ourselves and reduce the amount of generated CSS for transitions (as seen in the code below), we’re still not making progress in terms of extending support.

.blinds { background: linear-gradient(90deg, #f90 var(--pos, 0%), #444 0) 50%/ 5em; transition: .3s ease-in-out; :checked ~ & { --pos: 100%; }
}
Animated gif. The blinds opening effect happens on checking an 'open blinds' checkbox, while unchecking it triggers the closing effect.
Open/close blinds on checking/unchecking the checkbox (live demo, Edge only).

Enter Houdini, which allows us to register custom properties and then animate them. Currently, this is only supported by Blink browsers behind the Experimental Web Platform features flag, but it’s still extending support a bit from Edge alone.

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

Going back to our example, we register the --pos custom property:

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

Note that <length-percentage> means it accepts not only length and percentage values, but also calc() combinations of them. By contrast, <length> | <percentage> only accepts length and percentage values, but not calc() combinations of them.

However, doing this doesn’t make any difference in Chrome, even with the flag enabled, probably because, in the case of transitions, what’s being transitioned is the property whose value depends on the CSS variable and not the CSS variable itself. And since we generally can’t transition between two background images in Chrome in general, this fails as well.

It does work in Edge, but it worked in Edge even without registering the --pos variable because Edge allows us to transition between gradients in general.

What does work in Blink browsers with the flag enabled is having an animation instead of a transition.

html { background: linear-gradient(90deg, #f90 var(--pos, 0%), #444 0) 50%/ 5em; animation: blinds .85s ease-in-out infinite alternate;
} @keyframes blinds { to { --pos: 100%; } }

However, this is now not working in Edge anymore because, while Edge can animate between gradient backgrounds, it cannot do the same for custom properties.

So we need to take an alternative approach for Edge here. This is where @supports comes in handy, since all we have to do is check whether a -ms- prefixed property is supported.

@function grad($pos: 100%) { @return linear-gradient(90deg, #f90 $pos, #444 0);
} html{ /* same as before */ @supports (-ms-user-select: none) { background-image: grad(0%); animation-name: blinds-alt; }
} @keyframes blinds-alt { to { background-image: grad() } }

Stop positions aren’t the only thing we can animate this way. We can do the same thing for the gradient angle. The idea behind it is pretty much the same, except now our animation isn’t an alternating one anymore and we use an easeInOutBack kind of timing function.

@function grad($ang: 1turn) { @return linear-gradient($ang, #f90 50%, #444 0);
} html { background: grad(var(--ang, 0deg)); animation: rot 2s cubic-bezier(.68, -.57, .26, 1.65) infinite; @supports (-ms-user-select: none) { background-image: grad(0turn); animation-name: rot-alt; }
} @keyframes rot { to { --ang: 1turn; } } @keyframes rot-alt { to { background-image: grad(); } }

Remember that, just like in the case of stop positions, we can only animate between gradient angles expressed in the same unit in Edge, so calling our Sass function with grad(0deg) instead of grad(0turn) doesn’t work.

And, of course, the CSS variable we now use accepts angle values instead of lengths and percentages:

CSS.registerProperty({ name: '--ang', syntax: '<angle>', initialValue: '0deg'
});
Animated gif. Shows a top to bottom gradient with an abrupt change from grey to orange at 50%. The angle of this gradient is animated using a easeInOutBack timing function (which overshoots the end values at both ends).
Sweeping around (live demo, Blink browsers with flag and Edge only).

In a similar fashion, we can also animate radial gradients. And the really cool thing about the CSS variable approach is that it allows us to animate different components of the gradient differently, which is something that’s not possible when animating gradients as a whole the way Edge does (which is why the following demos don’t work as well in Edge).

Let’s say we have the following radial-gradient():

$p: 9%; html { --x: #{$p}; --y: #{$p}; background: radial-gradient(circle at var(--x) var(--y), #f90, #444 $p);
}

We register the --x and --y variables:

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

Then we add the animations:

html { /* same as before */ animation: a 0s ease-in-out -2.3s alternate infinite; animation-name: x, y; animation-duration: 4.1s, 2.9s;
} @keyframes x { to { --x: #{100% - $p} } }
@keyframes y { to { --y: #{100% - $p} } }

The result we get can be seen below:

Animated GIF. Shows a moving glowing orange light on a grey background. This is achieved by animating the coordinates of the central point of a radial gradient independently with the help of CSS variables and Houdini.
Moving light (live demo, Blink browsers with flag only).

We can use this technique of animating the different custom properties we use inside the gradient function to make the blinds in our initial example close the other way instead of going back. In order to do this, we introduce two more CSS variables, --c0 and --c1:

$c: #f90 #444; html { --c0: #{nth($c, 1)}; --c1: #{nth($c, 2)}; background: linear-gradient(90deg, var(--c0) var(--pos, 0%), var(--c1) 0) 50%/ 5em;
}

We register all these custom properties:

CSS.registerProperty({ name: '--pos', syntax: '<length-percentage>', initialValue: '0%'
}); CSS.registerProperty({ name: '--c0', syntax: '<color>', initialValue: 'red'
}); /* same for --c1 */

We use the same animation as before for the position of the first stop --pos and, in addition to this, we introduce two steps() animations for the other two variables, switching their values every time an iteration of the first animation (the one changing the value of --pos) is completed:

$t: 1s; html { /* same as before */ animation: a 0s infinite; animation-name: c0, pos, c1; animation-duration: 2*$t, $t; animation-timing-function: steps(1), ease-in-out;
} @keyframes pos { to { --pos: 100%; } } @keyframes c0 { 50% { --c0: #{nth($c, 2)} } }
@keyframes c1 { 50% { --c1: #{nth($c, 1)} } }

And we get the following result:

Animated GIF. Shows the blinds effect with the blinds closing the other way. Once the vertical orange strips (openings) have expanded horizontally such that they cover the whole background, they don't start contracting again. Instead, vertical grey orange strips start expanding from nothing until they cover the whole background.
Another version of the blinds animation (live demo, Blink browsers with flag only).

We can also apply this to a radial-gradient() (nothing but the background declaration changes):

background: radial-gradient(circle, var(--c0) var(--pos, 0%), var(--c1) 0);
Animated gif. We start with a grey background and we have an orange disc growing from nothing in the middle until it covers everything. Then we have a grey disc growing from nothing in the middle until it covers the entire background and we're back where we started from: a grey background.
Growing discs (live demo, Blink browsers with flag only).

The exact same tactic works for conic-gradient() as well:

background: conic-gradient(var(--c0) var(--pos, 0%), var(--c1) 0);
Animated gif. We start with a grey background and we have an orange pie slice (circular sector) growing from nothing to covering everything around the central point. Then we have a grey pie slice growing from nothing to covering everything around the central point and we're back where we started from: a grey background.
Growing slices (live demo, Blink browsers with flag only).

Repeating gradients are also an option creating a ripple-like effect in the radial case:

$p: 2em; html { /* same as before */ background: repeating-radial-gradient(circle, var(--c0) 0 var(--pos, 0px), var(--c1) 0 $p);
} @keyframes pos { 90%, 100% { --pos: #{$p} } }
Animated gif. We start with a grey background and we have concentric orange circles growing outwards from really thin until they meet and cover everything, so now it looks like we have an orange background. Then we have grey circles growing outwards from really thin until they cover the entire background and we're back where we started from: a grey background.
Ripples (live demo, Blink browsers with flag only).

And a helix/rays effect in the conic case:

$p: 5%; html { /* same as before */ background: repeating-conic-gradient( var(--c0) 0 var(--pos, 0%), var(--c1) 0 $p);
} @keyframes pos { 90%, 100% { --pos: #{$p} } }
Animated gif. We start with a grey background and we have orange rays growing clockwise from really thin until they meet and cover everything, so now it looks like we have an orange background. Then we have grey rays growing clockwise from really thin until they cover the entire background and we're back where we started from: a grey background.
Growing rays (live demo, Blink browsers with flag only).

We can also add another CSS variable to make things more interesting:

$n: 20; html { /* same as before */ background: radial-gradient(circle at var(--o, 50% 50%), var(--c0) var(--pos, 0%), var(--c1) 0); animation: a 0s infinite; animation-name: c0, o, pos, c1; animation-duration: 2*$t, $n*$t, $t; animation-timing-function: steps(1), steps(1), ease-in-out;
} @keyframes o { @for $i from 0 to $n { #{$i*100%/$n} { --o: #{random(100)*1%} #{random(100)*1%} } }
}

We need to register this variable for the whole thing to work:

CSS.registerProperty({ name: '--o', syntax: '<length-percentage>+', initialValue: '50%'
});

And that’s it! The result can be seen below:

Animated gif. We start with a grey background and we have an oranges disc, randomly positioned, growing from nothing until it covers everything, so now it looks like we have an orange background. Then we have grey disc, randomly positioned, growing from nothing until it covers the entire background and we're back where we started from: a grey background.
Randomly positioned growing discs (live demo, Blink browsers with flag only).

I’d say the future of changing gradients with keyframe animations looks pretty cool. But in the meanwhile, for cross-browser solutions, the JavaScript way remains the only valid one.

The post The State of Changing Gradients with CSS Transitions and Animations appeared first on CSS-Tricks.

Making CSS Animations Feel More Natural

It used to be that designers designed and coders coded. There was no crossover, and that’s the way it was. But with the advent of CSS transitions and animations, those lines are blurring a bit. It’s no longer as simple as the designer dictating the design and the coder transcribing—designers must now know something about code, and coders must know something about design in order to effectively collaborate.

As an example, let’s say a designer asks a developer to make a box bounce. That’s it—no additional instruction. Without some cross-knowledge and a common vocabulary, both sides are a little lost in this communication: the developer doesn’t have enough information to fully realize the designer’s vision, but the designer doesn’t really know what the options are and how to communicate them. With a very basic interpretation, you might end up with something that looks like this:

See the Pen Bouncing Box 1 by Brandon Gregory (@pulpexploder) on CodePen.

Not very exciting. Although, to be fair, this does meet all of the criteria given. We can definitely do better than this, though.

The first thing to look at is the timing function. In the above example, we’re using a linear timing function, which means that the box is constantly moving at the same speed. In some cases, this is desirable; however, in the real world, motion usually doesn’t work like that.

An easy fix is to simply change the timing function to ease. This makes the beginning and ending of each animation a little slower than the middle part, which adds a more natural look to some animations. Here’s the box with the easing function turned on:

See the Pen Bouncing Box 2 by Brandon Gregory (@pulpexploder) on CodePen.

This is a slight improvement, but there’s still a lot of work to be done. The box still looks mechanical and stiff, with the same animation occurring in the same timeframe over and over. Adding a slight delay between bounces adds some visual contrast that seems a little more natural:

See the Pen Bouncing Box 3 by Brandon Gregory (@pulpexploder) on CodePen.

The box now looks like it’s jumping rather than simply moving up and down. There’s a little wind-up and cool-down between jumps that mimics what a live creature might do if given the same instruction. Even though we have no reference for what a jumping box would look like, we all have a pretty good idea of what a jumping creature would look like. Because we know what would happen in nature, by mimicking that, the animation feels more natural. But we can do more to make that wind-up feel a little more weighty.

If you watch cartoons, you’ll notice that natural movements are often exaggerated, creating a caricature of real life. When done well, this can feel just as natural as something in the real world, with the added bonus of infusing a little charm and character into the animation.

At this stage, collaboration between the designer and developer is crucial — but many designers may not even be aware that these options exist. It may be up to the developer to pitch this possibility to the designer.

By adding some subtle distortion to the scale of the box, we can add a lot to the animation:

See the Pen Bouncing Box 4 by Brandon Gregory (@pulpexploder) on CodePen.

Now, the box has character. It feels alive. There are many things to tweak, but this is already moving much farther than the original instruction — in a very good way!

We’re going to go a step further and add a little rebound at the end of the jump:

See the Pen Bouncing Box 5 by Brandon Gregory (@pulpexploder) on CodePen.

The second bounce is making this feel more alive, but something still seems off. The bounce looks stiff compared to the rest of the animation. We need to add another bit of distortion like we did for the wind-up:

See the Pen Bouncing Box 6 by Brandon Gregory (@pulpexploder) on CodePen.

That subtle distortion at the end makes the rebound seem much more natural. Overall, a huge improvement from our basic linear bounce in the first example.

That right there may be exactly what we’re looking for, but further tweaks to the rate of movement can be made with a custom cubic Bézier curve:

See the Pen Bouncing Box 7 by Brandon Gregory (@pulpexploder) on CodePen.

Without both the designer and the developer aware of basic animation principles and controls, this level of customization is impossible. Really, this article just scratches the surface of both fields. If you’re a web designer or a web developer who works with designers, I’d strongly urge you to read up on both.

For animation principles, The Illusion of Life: Disney Animation by Ollie Johnston and Frank Thomas is a great primer on how to make that caricature of real life seem alive and real. With that common language in place, communication and collaboration between designers and developers becomes much easier.

For the technical controls and variations of CSS animation, the possibilities are nearly endless. Delay and timing are simple to adjust. As mentioned, if you don’t like the out-of-the-box ease timing function, it’s very possible to create your own using a cubic-bezier(). You can also adjust the level of distortion you want to bring the animation closer to or further from reality. The important thing is that both the designer and developer are thinking about these variations rather than blindly taking everything without customization. Shared knowledge and collaboration can make even simple animations into great ones.

More Resources

  • 12 basic principles of animation – Wikipedia post outlining the concepts introduced in The Illusion of Life.
  • The Guide to CSS Animation: Principles and Examples – Smashing Magazine article providing a comprehensive guide to CSS animations.
  • Animation in Design Systems – 24 Ways article by Sarah Drasner
  • Animation property – CSS-Tricks almanac entry covering the property and its values
  • Transition property – CSS-Tricks almanac entry covering the property and its values
  • CubicBezier.com – Resource to create custom animation curves with a user interface.

Making CSS Animations Feel More Natural is a post from CSS-Tricks