Transitions CSS et animations de survol, un guide interactif


Le monde des animations Web est devenu une jungle tentaculaire d’outils et de technologies. Des bibliothèques comme GSAP et Framer Motion et React Spring ont vu le jour pour nous aider à ajouter du mouvement au DOM.

La pièce la plus fondamentale et la plus critique, cependant, est l’humble transition CSS. C’est le premier outil d’animation que la plupart des développeurs front-end apprennent, et c’est un cheval de bataille. Même les vétérans de l’animation les plus grisonnants et les plus altérés utilisent encore souvent cet outil.

Il y a une profondeur surprenante dans ce sujet. Dans ce didacticiel, nous allons creuser et en apprendre un peu plus sur les transitions CSS et sur la façon dont nous pouvons les utiliser pour créer des animations luxuriantes et soignées.

L’ingrédient principal dont nous avons besoin pour créer une animation est un CSS qui change.

Voici un exemple de bouton qui se déplace au survol, sans animation:

Aire de jeux codée


Actualiser le volet des résultats

Cet extrait utilise le :hover pseudoclass pour spécifier une déclaration CSS supplémentaire lorsque la souris de l’utilisateur repose sur notre bouton, similaire à une onMouseEnter événement en JavaScript.

Pour déplacer l’élément vers le haut, nous utilisons transform: translateY(-10px). Alors que nous aurions pu utiliser margin-top pour ça, transform: translate est un meilleur outil pour le travail. Nous verrons pourquoi plus tard.

Par défaut, les changements dans CSS se produisent instantanément. En un clin d’œil, notre bouton s’est téléporté vers une nouvelle position ! Ceci est incongru avec le monde naturel, où les choses se passent progressivement.

Nous pouvons demander au navigateur de interpoler d’un état à l’autre avec le bien nommé transition propriété:

Aire de jeux codée


Actualiser le volet des résultats

transition peut prendre plusieurs valeurs, mais seulement deux sont requises :

  1. Le nom du bien que nous souhaitons animer

  2. La durée de l’animation

Si vous envisagez d’animer plusieurs propriétés, vous pouvez lui transmettre une liste séparée par des virgules :

When we tell an element to transition from one position to another, the browser needs to work out what each “intermediary” frame should look like.

For example: let’s say that we’re moving an element from left to right, over a 1-second duration. A smooth animation should run at 60fps, which means we’ll need to come up with 60 individual positions between the start and end.

Let’s start by having them be evenly-spaced:

To clarify what’s going on here: each faded circle represents a moment in time. As the circle moves from left to right, these are the frames that were shown to the user. It’s like a flipbook.

In this animation, we’re using a linear timing function. This means that the element moves at a constant pace; our circle moves by the same amount each frame.

There are several timing functions available to us in CSS. We can specify which one we want to use with the transition-timing-function property:

Or, we can pass it directly to the transition shorthand property:

linear is rarely the best choice — after all, pretty much nothing in the real world moves this way. Good animations mimic the natural world, so we should pick something more organic!

Let’s run through our options.

ease-out comes charging in like a wild bull, but it runs out of energy. By the end, it’s pootering along like a sleepy turtle.

Try scrubbing with the timeline; notice how drastic the movement is in the first few frames, and how subtle it becomes towards the end.

If we were to graph the displacement of the element over time, it’d look something like this:

When would you use ease-out? It’s most commonly used when something is entering from off-screen (eg. a modal appearing). It produces the effect that something came hustling in from far away, and settles in front of the user.

ease-in, unsurprisingly, is the opposite of ease-out. It starts slow and speeds up:

As we saw, ease-out is useful for things that enter into view from offscreen. ease-in, naturally, is useful for the opposite: moving something beyond the bounds of the viewport.

This combo is useful when something is entering and exiting the viewport, like a modal. We’ll look at how to mix and match timing functions shortly.

Note that ease-in is pretty much exclusively useful for animations that end with the element offscreen or invisible; otherwise, the sudden stop can be jarring.

Next up, ease-in-out. It’s the combination of the previous two timing functions:

This timing function is symmetrical. It has an equal amount of acceleration and deceleration.

I find this curve most useful for anything that happens in a loop (eg. an element fading in and out, over and over).

It’s a big step-up over linear, but before you go slapping it on everything, let’s look at one more option.

If I had a bone to pick with the CSS language authors when it comes to transitions, it’s that ease is poorly named. It isn’t descriptive at all; literally all timing functions are eases of one sort or another!

That nitpick aside, ease is awesome. Unlike ease-in-out, it isn’t symmetrical; it features a brief ramp-up, and a lot of deceleration.

ease is the default value — if you don’t specify a timing function, ease gets used. Honestly, this feels right to me. ease is a great option in most cases. If an element moves, and isn’t entering or exiting the viewport, ease is usually a good choice.

If the provided built-in options don’t suit your needs, you can define your own custom easing curve, using the cubic bézier timing function!

All of the values we’ve seen so far are really just presets for this cubic-bezier function. It takes 4 numbers, representing 2 control points.

Bézier curves are really nifty, but they’re beyond the scope of this tutorial. I’ll be writing more about them soon though!

In the meantime, you can start creating your own Bézier timing functions using this wonderful helper from Lea Verou:

Once you come up with an animation curve you’re satisfied with, click “Copy” at the top and paste it into your CSS!

You can also pick from this extended set of timing functions. Though beware: a few of the more outlandish options won’t work in CSS.

When starting out with custom Bézier curves, it can be hard to come up with a curve that feels natural. With some practice, however, this is an incredibly expressive tool.

Earlier, we mentioned that animations ought to run at 60fps. When we do the math, though, we realize that this means the browser only has 16.6 milliseconds to paint each frame. That’s really not much time at all; for reference, it takes us about 100ms-300ms to blink!

If our animation is too computationally expensive, it’ll appear janky and stuttery. Frames will get dropped, as the device can’t keep up.

Experience this for yourself by tweaking the new “Frames per second” control:

In practice, poor performance will often take the form of variable framerates, so this isn’t a perfect simulation.

Animation performance is a surprisingly deep and interesting area, well beyond the scope of this introductory tutorial. But let’s cover the absolutely-critical, need-to-know bits:

  1. Some CSS properties are wayyy more expensive to animate than others. For example, height is a very expensive property because it affects layout. When an element’s height shrinks, it causes a chain reaction; all of its siblings will also need to move up, to fill the space!

  2. Other properties, like background-color, are somewhat expensive to animate. They don’t affect layout, but they do require a fresh coat of paint on every frame, which isn’t cheap.

  3. Two properties — transform and opacity — are very cheap to animate. If an animation currently tweaks a property like width or left, it can be greatly improved by moving it to transform (though it isn’t always possible to achieve the exact same effect).

  4. Be sure to test your animations on the lowest-end device that your site/app targets. Your development machine is likely many times faster than it.

If you’re interested in learning more about animation performance, I gave a talk on this subject at React Rally. It goes deep into this topic:

Depending on your browser and OS, you may have noticed a curious little imperfection in some of the earlier examples:

Pay close attention to the letters. Notice how they appear to glitch slightly at the start and end of the transition, as if everything was locking into place?

This happens because of a hand-off between the computer’s CPU and GPU. Let me explain.

When we animate an element using transform and opacity, the browser will sometimes try to optimize this animation. Instead of rasterizing the pixels on every frame, it transfers everything to the GPU as a texture. GPUs are very good at doing these kinds of texture-based transformations, and as a result, we get a very slick, very performant animation. This is known as “hardware acceleration”.

Here’s the problem: GPUs and CPUs render things slightly differently. When the CPU hands it to the GPU, and vice versa, you get a snap of things shifting slightly.

We can fix this problem by adding the following CSS property:

will-change is a property that allows us to hint to the browser that we’re going to animate the selected element, and that it should optimize for this case.

In practice, what this means is that the browser will let the GPU handle this element all the time. No more handing-off between CPU and GPU, no more telltale “snapping into place”.

will-change lets us be intentional about which elements should be hardware-accelerated. Browsers have their own inscrutable logic around this stuff, and I’d rather not leave it up to chance.

There’s another benefit to hardware acceleration: we can take advantage of sub-pixel rendering.

Check out these two boxes. They shift down when you hover/focus them. One of them is hardware-accelerated, and the other one isn’t.

Code Playground


Actualiser le volet des résultats

C’est peut-être un peu subtil, selon votre appareil et votre écran, mais une boîte se déplace beaucoup plus facilement que l’autre.

Des propriétés comme margin-top ne peuvent pas sous-pixel-rendre, ce qui signifie qu’ils doivent arrondir au pixel le plus proche, créant un effet décalé et janky. transformquant à lui, peut se déplacer en douceur entre les pixels, grâce à la supercherie anti-aliasing du GPU.

Jetons un autre coup d’œil à notre bouton “Hello World” en hausse.

Dans l’état actuel des choses, nous avons une transition “symétrique” – l’animation d’entrée est la même que l’animation de sortie :

  • Lorsque la souris survole l’élément, il se décale de 10 pixels sur 250 ms

  • Lorsque la souris s’éloigne, l’élément se décale de 10 pixels sur 250 ms

Un petit détail mignon est de donner à chaque action ses propres paramètres de transition. Pour les animations de survol, j’aime rendre l’animation d’entrée rapide et vive, tandis que l’animation de sortie peut être un peu plus détendue et léthargique :

Aire de jeux codée


Actualiser le volet des résultats

Un autre exemple courant est celui des modaux. Il peut être utile pour les modaux d’entrer avec un ease-out animation, et de sortir avec un ease-in animation:

C’est un petit détail, mais il parle d’une idée beaucoup plus large.

Je crois que la plupart des développeurs pensent en termes de États: par exemple, vous pourriez regarder cette situation et dire que nous avons un état “survolé” et un état par défaut. Au lieu de cela, et si nous pensions en termes de Actions? Nous animons en fonction de ce que fait l’utilisateur, en pensant en termes d’événements et non d’états. Nous avons une animation d’entrée de la souris et une animation de sortie de la souris.

Tobias Ahlin montre comment cette idée peut créer des animations sémantiquement significatives de niveau supérieur dans son article de blog, Meaningfun Motion with Action-Driven Animation.

Eh bien, nous avons parcouru un long chemin dans notre quête pour maîtriser les transitions CSS, mais il reste encore quelques détails à régler. Parlons des délais de transition.

Je crois que presque tout le monde a déjà vécu cette expérience frustrante :

Image reproduite avec l’aimable autorisation de Ben Kamens

En tant que développeur, vous pouvez probablement comprendre pourquoi cela se produit : la liste déroulante ne reste ouverte que lorsqu’elle est survolée ! Lorsque nous déplaçons la souris en diagonale pour sélectionner un enfant, notre curseur sort des limites et le menu se ferme.

Ce problème peut être résolu de manière assez élégante sans avoir besoin d’utiliser JS. On peut utiliser transition-delay!

transition-delay allows us to keep things status-quo for a brief interval. In this case, when the user moves their mouse outside .dropdown-wrapper, nothing happens for 300ms. If their mouse re-enters the element within that 300ms window, the transition never takes place.

After 300ms elapses, the transition kicks in normally, and the dropdown fades out over 400ms.

When an element is moved up or down on hover, we need to be very careful we don’t accidentally introduce a “doom flicker”:

Warning: This GIF includes flickering motion that may potentially trigger seizures for people with photosensitive epilepsy.

You may have noticed a similar effect on some of the demos on this page!

The trouble occurs when the mouse is near the element’s boundary. The hover effect takes the element out from under the mouse, which causes it to fall back down under the mouse, which causes the hover effect to trigger again… many times a second.

How do we solve for this? The trick is to separate the trigger from the effect. Here’s a quick example:

Code Playground


Actualiser le volet des résultats

Notre <button> a maintenant un nouvel enfant, .background. Cette étendue abrite tous les styles cosmétiques (couleur d’arrière-plan, polices, etc.).

Lorsque nous passons la souris sur le bouton plain-jane, l’enfant regarde au-dessus. Le bouton, cependant, est fixe.

Essayez de décommenter le outline pour voir exactement ce qui se passe !

Quand je vois une animation bien ficelée sur le web, je réagis avec joie et joie. Les gens sont différents, cependant, et certaines personnes ont un très réaction différente : nausées et malaise.

J’ai déjà écrit sur le respect de “préfère le mouvement réduit”, un paramètre au niveau du système d’exploitation que les utilisateurs peuvent basculer pour exprimer une préférence pour moins de mouvement. Appliquons ces leçons ici, en désactivant les animations pour les personnes qui en font la demande :

Ce petit ajustement signifie que les animations seront résolues immédiatement pour les utilisateurs qui sont entrés dans leurs préférences système et ont coché une case.

En tant que développeurs front-end, nous avons une certaine responsabilité pour nous assurer que nos produits ne causent pas de dommages. Il s’agit d’une étape rapide que nous pouvons effectuer pour rendre nos sites/applications plus conviviaux et plus sûrs.

Les transitions CSS sont fondamentales, mais cela ne veut pas dire qu’elles sont faciles. Il y a une quantité surprenante de profondeur pour eux; même dans ce long article de blog, j’ai dû supprimer certaines choses pour que cela reste gérable !

Les animations Web sont plus importantes que ne le pensent la plupart des développeurs. Une seule transition ici ou là ne fera ni ne détruira une expérience, mais elle s’additionne. Dans l’ensemble, des animations bien exécutées peuvent avoir un effet étonnamment profond sur l’expérience globale de l’utilisateur.

Les transitions peuvent donner l’impression qu’une application est “réelle”. Ils peuvent offrir des commentaires et communiquer de manière plus viscérale que la copie seule. Ils peuvent apprendre aux gens comment utiliser vos produits. Ils peuvent susciter de la joie.

Alors, j’ai un aveu à vous faire : ce tutoriel a été extrait directement de mon cours CSS, CSS pour les développeurs JavaScript. Si vous avez trouvé ce tutoriel utile, sachez que ce n’est que la pointe de l’iceberg !

Mon cours est conçu pour vous donner confiance en CSS. Nous explorons comment la langue vraiment fonctionne, en construisant un modèle mental que vous pouvez utiliser pour résoudre n’importe quel défi de mise en page/interface utilisateur.

Ce n’est pas comme les autres cours que vous avez suivis. Il est construit sur la même pile technologique que ce blog, et il y a donc beaucoup de contenu interactif riche, mais il y a aussi des petites vidéos, des tonnes d’exercices et des projets inspirés du monde réel où vous pouvez tester vos connaissances. Il y a même des mini-jeux !

Apprenez-en plus et voyez si vous en tireriez profit sur https://css-for-js.dev:

Enfin, aucune leçon interactive n’est complète sans un Mode bac à sable! Jouez avec tous les paramètres précédents (et quelques nouveaux !) et créez de l’art génératif avec ce widget ouvert :

Dernière mise à jour

20 novembre 2022