As designers and developers, we’re always searching for ways to produce interfaces that are simpler, more performant, and more fun for our users. Of these three factors, the former two have come a long way in terms of research and coverage over the past few years. The third, however, encompasses newer territory: interactions and visual flourish whose primary intent is to delight and instill a sense of fulfillment in your users. UI animations are a wonderful way to imbue your website or application with these qualities.
In the overall timeline of web design and development, animation is still a relatively new subject — it’s only within the past few years that we’ve had wide enough browser support to consider using CSS-based animations, rather than the JS-based solutions we’d been using previously (I’m sure we all recall the days of using jQuery to manipulate CSS properties and achieve animation effects). Now that we’re able to employ CSS animations at a larger scale, we’re finding that there’s widespread need to develop techniques to manage these animations, particularly for two high-level use cases:
- Sequential (or “stacked”) animations (e.g., a button that slides into view, then increases slightly in size, then morphs from a square into a circle before finally settling into place)
- Concurrent animations (e.g., a masthead that collapses vertically in size while buttons inside it change color and a logo element inside it grows in size before settling into place)
We’ll make the assumption that each of these approaches is being applied to one top-level element — that is, a masthead, card, panel, button, or other element consisting of any number of child elements.
Now, there are a variety of tools and guidelines used with respect to approach #1 — sequential animations seem to be a subject that’s often tackled when it comes to UI animation. The desire to animate one top-level element from one state, to another, to another is paramount to creating complex, multi-step animations that contribute to a lively, interesting user interface.
We’re not discussing sequential animations here.
Concurrent animations haven’t received the same level of coverage and exploration that sequential animations have. One can surmise that this is primarily for two reasons:
- Sequential animations are a more commonly-tackled problem that apply to a broader set of UI-related tasks
- Managing concurrent animations is more convoluted and doesn’t lend itself as cleanly to clear, pragmatic solutions
Let’s discuss #2 and build some pathways towards elegant management of concurrent animations.
To start, let’s begin with a sample element that we can use to demonstrate our concurrent animations. Building upon our previous description, I’ve whipped up a quick sample of the masthead briefly described above:
To explain what’s happening here:
We’ve got a masthead component, its HTML architected following conventions set forth in Harry Roberts’ article “More Transparent UI Code with Namespaces.” Then, we’ve written Sass for this HTML following BEM conventions and principles that reduce cyclomatic complexity: leveraging low-specificity selectors, eliminating nesting (except where dictated in the HTML spec, such as list items within an unordered list), and building upon the aforementioned namespaces. Finally, we’ve written JS to add a top-level modifier class to the masthead itself when the trigger button is clicked, as well as child element-specific animation classes, which are then removed from the child elements when the animations complete.
This may seem needlessly complex, but there’s a method to the madness.
Think about how you’d typically write the HTML, CSS, and JS to accomplish this particular masthead. You might structure the HTML similarly to the example, and then add specific CSS classes for the particular animations, leveraging the animation-fill-mode property to make sure the properties stick once the animation completes. Lastly, you’d use JS to apply the animation classes to the individual elements, requiring them to hang around after the animations have completed in order to achieve the desired layout/visuals.
There are a few problems with using these conventional, established patterns to create this masthead and its related animations:
- By using animation-fill-mode to achieve a layout, we’re relying on animations to do a lot more than just animate, which fundamentally spits in the face of our desire for clean, understandable, single-responsibility code. Even worse, our layout now absolutely requires not only JS to add the animation-based classes, but also any number of CSS animation features to achieve layout, making it quite difficult to predict browser support for our components in their various states
- By omitting a state-based modifier class on the overall masthead element, we’re reducing readability and maintainability by forcing other developers to dig through individual child elements in order to figure out what state they’re in (and therefore what they currently look like). Additionally, this makes it very difficult to lump multiple child elements together under one “umbrella” state — something we’ll see the benefit of shortly
- By not using an established architectural and namespacing pattern for our CSS classes, we’re making it much harder to track down which styles apply to which of our elements, as well as which animations apply to which of our elements in a certain state
By architecting this particular component in the way that most immediately dawns upon us, we’re probably overlooking some pretty glaring flaws that will come back to haunt us. Rather than creating an element that’s readable, maintainable, adheres to single-responsibility principles, and is widely-supported across devices and browsers, we’ve created an element that possesses a variety of faults that will affect our end-users as well as other developers.
This can be remedied. By emphasizing the importance of modularity, proper namespacing, and concepts like single-responsibility, we can rewrite this component in a way that’s much more viable. Let’s talk about some of the accommodations we’re making that are demonstrated in the above code sample:
- We’re using three key CSS namespaces to separate concerns and make it clear what’s happening in which state: the c- namespace for the component itself and related elements, the is-/has- boolean namespace for the states of the component, and an a- namespace for CSS animations tied to the utilization of keyframes
- We’re defining the default appearance of the component, as well as its appearance while a state is applied, using a chained CSS class on the top-level element and a descendent selector (via Sass) to child elements that will change in appearance. Note that neither the default state nor a modified state of the component require CSS animations to be achieved! The goal here is to create a component that can be properly displayed even if CSS animations aren’t supported on a browser or fail for whatever reason
- We’re following some sensible conventions for the naming of classes and keyframes that fall outside typical BEM/namespacing guidelines: the a- animation class describes, in layman’s terms, what the animation’s doing, and the keyframes name is a combination of the element’s class plus the animation class, separated by a double hyphen (we could modify this to suit our specific application’s needs: what happens when we need responsive animations or are animating an element that has a BEM modifier?)
- When we add our modifier class to the top-level component via JS, we’re also adding the animation classes to the specific top-level and child elements. Note that this particular technique may require us to match our pre-modifier styles at 0% in our keyframes and our post-modifier styles at 100%. Remember: the goal is to completely decouple our animations from our component’s state, using them to augment our component in an additive manner, rather than making them a hard requirement to achieve state
- Lastly, we’re using the technique described in this simple StackOverflow answer to remove the animation classes when the animations complete, leaving us with a component that looks the way we want, has simple-to-trace CSS classes on (and in) it, and doesn’t force us to guess whether certain CSS animation properties were used to achieve the current visual state
Now, this approach has its faults: above all else, it will result in a lot of JS, both for adding the animation classes and then removing them when animations complete, and this pair of actions has to be taken on every single animated element. As you can imagine, the addClass() calls will begin to litter your editor before long, particularly if you’re working with a component that contains many animated children.
Additionally, there remains the question of what will happen to these animation classes if they fail to complete (or otherwise fire the “animationend” event) — for instance, in older browsers. We can probably assume they’d simply hang around on an element, functionless phantoms needlessly strewn throughout our JS and weighing down our HTML.
Lastly, because our animations are now decoupled from the state of our component, we’ve unfortunately introduced a new issue: multiple animation classes could be added to an element simultaneously — for instance, if a component’s state is toggled multiple times in rapid succession. This could result in weird, half-finished animations firing on top of one another, creating buggy, broken animations.
The second problem of these three is the easiest to solve: a feature detection library such as Modernizr would allow us to wrap our JS addClass() calls containing our animation classes in simple checks to verify a browser supports CSS animations. If not, just add the boolean class to change state, ignore the animation classes, and be on your way!
The first and third problems, however, are a bit more difficult to solve.
What we need is a two-pronged approach: the combination of a state machine (to manage a component’s state by supplying a configuration object containing references to the component’s elements and their related animations given a specific state) and a utility library (to manage the transition between states, adding and removing the aforementioned animation classes per element and allowing a change of state to occur only when the previous state has been 100% achieved and all animations have finished).
Allow the concepts and code we’ve seen here some time to sink in and look forward to Part 2!