Most user interface animations fall into two categories: static (i.e., timeline-based) and reactive. A reactive animation is one involving changes based on events, rather than based on a fixed timeline. While many web-based user interfaces make use of static animations, reactive animations can create a truly interactive user experience.
With CSS Custom Properties (a.k.a. CSS Variables) now widely supported in modern browsers, adding reactive animations to user interfaces can be done with ease by taking advantage of CSS features such as inheritance, relative units, and transitions.
You might also like: Using CSS Animations to Guide a Better Ecommerce Experience.
CSS variables
Despite its common name, CSS variables aren't true variables; however, they certainly act like them. Their full name—CSS custom properties—makes it clearer: they behave as properties, which are subject to cascading, inheritance, and media queries.
A CSS variable is defined in a ruleset by prepending the variable name with two dashes:
css
:root {
--my-color: red;
--button-font-size: 16px;
}
Above, I'm setting the CSS variable on the :root
selector. Its value can then be referenced within any selector that can inherit that custom property, using the var()
function, where the first argument is the name of the CSS variable, and the second (optional) is the default value:
css
.ui-button {
background-color: var(--my-color, blue);
font-size: var(--button-font-size);
}
CSS variables and JavaScript
Now, the key to making these CSS variables truly dynamic is by setting them via JavaScript. This is done with the .setProperty()
method on an element's .style
property, like so:
js
const el = document.documentElement;
// set custom property
el.style.setProperty(
'--my-color', // custom property name, with dashes
'red' // custom property value, as a string or number
);
// get custom property
// (only if defined on element)
el.style.getPropertyValue('--my-color');
// => 'red'
The browser will automatically infer native CSS value types from string values, such as the color red from 'red'
, or 15 pixels from '15px'
. However, this does incur some overhead. For performance and flexibility, it is best to use unitless number values for dynamically set CSS variables whenever possible:
js
// not unitless - incurs overhead
el.style.setProperty('--x', '30px');
// unitless
el.style.setProperty('--x', 30);
Unitless values can be converted to values with units by use of calc()
in CSS:
css
.ui-button {
/* convert unitless --x value to pixels */
transform: translateX(calc(var(--x, 0) * 1px));
}
By using calc()
, we can be versatile with CSS variables by combining them, using them with relative units, and more.
Getting reactive with CSS variables
Setting CSS variables in JavaScript allows for dynamic styling, but to create interactive experiences with reactive animations, they should be set in response to events. Events, in a general sense, can originate from anywhere, such as from user interactions like mouse events, external events like audio input, or from other animations themselves.
To make this easier, let's create a utility function that sets CSS variables on an element:
js
const docEl = document.documentElement;
function styleVar(property, value, element = docEl) {
element.style.setProperty(property, value);
}
For example, let's read values from the 'mousemove'
event and feed them into CSS variables:
js
docEl.addEventListener('mousemove', (event) => {
styleVar('--mouse-x', event.clientX);
styleVar('--mouse-y', event.clientY);
});
Now, whenever the mouse moves in the document, the --mouse-x
and --mouse-y
custom properties will be updated with the respective integer values. You can witness this by inspecting the DOM and observing the style
attribute of the <html>
element.
We can use these values directly in our CSS, without being concerned about how the values got there; just that they are present:
css
.ball {
transform:
translateX(calc(var(--mouse-x, 0) * 1px))
translateY(calc(var(--mouse-y, 0) * 1px));
}
Another application of this is to create "parallax" effects by listening to the 'scroll'
event and passing the .scrollX
or .scrollY
event properties:
js
window.addEventListener('scroll', e => {
styleVar('--scroll-y', e.scrollY);
});
Performance
What's great about CSS custom properties is that they work with the natural inheritance and cascade behavior of CSS. This has immediate benefits for both performance and code organization, for a few reasons:
- Instead of having to directly update style values for every affected element, only a single style value is affected (the custom property) and the elements are automatically updated
- There is no need to keep track of added and removed DOM nodes, so there's less overhead in applying styles to dynamic layouts
- Dynamic styling becomes simplified to a provided value, rather than a direct styling
However, one important thing to keep in mind is that currently, CSS custom properties are always inherited. When a custom property is applied to an element, the styles of the element and all of its descendants must be recalculated. For large DOM trees, this can be a performance hit.
To mitigate this, it's important to only apply custom properties to the deepest parent element affected. For example, if a button relies on a dynamically applied custom property, use .setProperty()
on that button directly instead of on the root element, whenever possible.
Using requestAnimationFrame()
can also be an opportunity to prevent jank and improve performance when dynamically styling with custom properties. Since everything visual on the page is rendered in sync with animation frames (ideally at 60 frames per second), be mindful of event handlers that can possibly emit events at a faster rate, such as scroll events.
You might also like: Using Animation to Improve Mobile App User Experience.
Accessibility and progressive enhancement
For people who prefer not to see animations for various reasons (including vestibular disorders), it’s important to either reduce or completely disable static or dynamic animations. This can be done with the prefers-reduced-motion
media query:
css
/* No motion preferences specified */
@media screen and (prefers-reduced-motion: no-preference) {}
/* Reduced motion requested */
@media screen and (prefers-reduced-motion: reduce) {
*, *:before, *:after {
animation: none !important;
}
}
However, reactive animations with CSS custom properties are not necessarily tied to animations. Thankfully, we can specify static values for these custom properties and force these values to be used:
css
@media screen and (prefers-reduced-motion: reduce) {
:root {
--mouse-x: 0 !important;
--mouse-y: 0 !important;
}
}
This will ensure that the values do not change and cause unnecessary motion for the user.
Whereas CSS custom properties are supported in all modern browsers, legacy browsers (such as older versions of IE) might not support them. CSS custom properties can be considered progressive enhancements in such cases, and static values can be substituted:
css
.box {
color: green; /* will be used if custom properties are not supported */
color: var(--my-color, green);
}
Examples
See the Pen Meshi the CSS Dog Reactive by David Khourshid (@davidkpiano) on CodePen.
In this pen, we're listening to the 'change'
event on the range element, and setting the --value
custom property to the range's value (from 0 to 2). Using this value, we can control the overall animation duration of the dog, which is comprised of many elements. Instead of updating each element, only one custom property needs to be updated, and the animation stays in sync, with the updated duration.
See the Pen Alex the CSS Husky Reactive by David Khourshid (@davidkpiano) on CodePen.
This pen was based on a pen with statically-defined animations. The difference is that this one replaces some of the values of many of the elements with custom properties, using var(--mouse-x, 0)
and var(--mouse-y, 0)
to control various translations and rotations. The end effect is that the dog ends up following the mouse's location.
See the Pen Magic Gradient by Oliver Turner (@oliverturner) on CodePen.
This pen takes a different approach to using custom properties, by styling HSL color values based on the mouse position. This is done by listening to the 'mousemove'
event, and interpolating the clientX
and clientY
values into HSL color values, and then setting them as custom properties for the gradient, as --grad-start
and --grad-end
. These are then used in the stylesheet to dynamically create the gradient as the user moves their mouse.
BasicScroll is a library created by Tobias Reich that uses the same principles of setting CSS custom properties with reactive values. These values can then be used directly in the stylesheet and achieve a variety of effects with greater performance than manually styling elements. This is because the scroll values are throttled with requestAnimationFrame
so that they are only updated when necessary (i.e., when the visible screen changes), and the custom properties can be isolated to the elements that will use those values.
You might also like: Pitching Animation: How to Talk About Motion in a Design-Centric Way.
Adding dimension
CSS custom properties (a.k.a. CSS variables) bring a whole new, dynamic dimension to CSS. Instead of directly styling elements with JavaScript, you now have the ability to assign dynamic values to custom properties via JavaScript and use them in your CSS. Besides the separation of concerns and the performance enhancements, styles applied via these custom properties are inspectable in Chrome, Firefox, and Edge’s dev tools. Make sure to only apply dynamic custom properties to the lowest scope of elements as possible, and to throttle style applications on requestAnimationFrame()
to limit unnecessary restyles.
For more information and examples, check out the slides from my Getting Reactive with CSS presentation at CSSConf EU 2017.
Read more
- 5 Easy-to-Implement Website Maintenance Tips for Your Client's Ecommerce Business
- Getting the Most Out of Chrome Developer Tools: 4 Modern Features You Need to Know
- Free Webinar] Getting Started with Sass and Shopify
- Free Webinar] Developing with Shopify: Using JavaScript to Add Ecommerce to Any Website
- A Modern Approach to CSS Pt. 2: Building Resources
Have you experimented with reactive animations? Tell us in the comments below!