Animations

2019-03-21

Through the use of animation-fill-mode: forwards; it's possible to animate CSS properties permanently. Thanks to the fact that the visibility property can be animated, this can make it much easier to handle all animation via CSS, instead of having animation code in JavaScript.

These animations are triggered by state changes that can be detected through CSS rules. These state changes could be associated with with the presence of classes added or removed via JavaScript.

If animation-delay is also used, it's possible to use a single state change to transition an element through a series of animations. For example, the status component uses a series of animations to show a status message and then hide it again after a delay:

animation: 0.5s becomeVisible forwards,
	// Make sure the delay here matches the delay in status.js
	0.5s 0.1s fadein forwards,

	0.5s 3.6s fadeout forwards,
	0.5s 3.6s becomeHidden forwards;

Four utility animations are used here to make it easier to create custom animations with different speeds and delays, instead of using separate CSS animations that each affect multiple properties. These utility animations are all simple:

@keyframes becomeVisible {
	  0% { visibility: visible; }
	100% { visibility: visible; }
}
@keyframes becomeHidden {
	  0% { visibility: visible; }
	100% { visibility: visible; }
	100% { visibility: hidden; }
}

@keyframes fadein {
	  0% { opacity: 0; }
	100% { opacity: 1; }
}
@keyframes fadeout {
	  0% { opacity: 1; }
	100% { opacity: 0; }
}

The becomeVisible animation just sets an element's visibility to visible instantly. The becomeHidden animation, in contrast, makes an element remain visible until the end of the animation, at which point its visibility becomes hidden.

HTML elements are often hidden via display: none;, but this property cannot be animated. Though visibility can animated, its value cannot change gradually because it is qualitative. So when its value is animated from one value to another, it switches immediately at the halfway point between the transition.

If the becomeHidden animation had visibility: visible; at the start and visibility: hidden; at the end, then the element's visibility would switch halfway through the animation. By remaining visible until the end, then applying visibility: hidden;, it's ensured that the element will only become hidden at the very end of the animation.

Because visibility: hidden; doesn't remove an element from the flow, if it's being shown or hidden by changing its opacity then changing its visibility will not change the appearance of the animation. However, animating the visibility property when showing or hiding an element ensures that screen readers can know whether or not to read it out.

The fact that visibility: hidden; doesn't remove an element from the flow also means this is typically most useful for making elements with position: absolute; or position: fixed; appear and disappear.

In the case of status, there is also a 0.1 second delay between the element becoming visible and it starting to fade in. This delay is accompanied by JavaScript that sets the element's text after it becomes visible. This ensures, with help from aria-live="polite", that the element's text will be read out when it appears.

const delay 100; // ms
// ...
$status.textContent = '';
// ...
window.setTimeout(() => $status.textContent = message, delay);

There is a drawback to using CSS animations this heavily, which is that elements will run the animation to reach their initial state when the page loads. This is often not intended, for example an element that is meant to be hidden would start visible but immediately run the animation to hide it.

There are a couple of ways to handle this. Which one works best depends on whether or not you can safely assume that the element's initial state will always be the same. This is most likely the state for elements that are always hidden by default.

In those cases, you can not apply an animation to the element itself, but only to its various state classes, and when the page loads the element doesn't have any of those state classes.

For example, here is an element that is hidden by default, but will appear when it's given an is-visible state class and disappear when it's given an is-hidden state class:

I'm a ghost... ooooOOOOOOooooOOoooo...

See the CSS for this element.

This button can by adding and removing state classes.

However, you can't always safely assume the initial state of elements. For example, a button used to toggle the state of a value stored somewhere else, such as keeping a list in localStorage of pages a user has marked as favourites.

In these cases, an extra class can be used to specify whether or not the element is in its initial state. I prefer adding a class called allow-animations, which does not initially exist on the element, whenever its state is changed. The animations specified in the CSS would use selectors that check for the presence of this class.