Module 8 - Animations
Transforms
-
The transform function allows elements to change in some way
-
translate can shift an element horizontally or vertically
- Its direction can be set with positive and negative values
- Setting a length unit as a percentage will refer to the element's own size rather than the space in the container
- The item's in-flow position is unaffected in any layout mode, other adjacent elements aren't manipulated
- translateX and translateY properties perform the same thing but for a single axis
transform: translate(0px, 0px);
-
scale takes in a unitless value to shrink or grow an element by a factor
- Note that all descendants of the element are also scaled
- scale can also take in two values to scale the x and y axis independently
transform: scale(2.5);
transform: scale(1.5, 2.5);
-
rotate just rotates the elements, typically using the deg unit
- The turn unit also exists, where 1turn is equivalent to 360deg
transform: rotate(180deg);
transform: rotate(0.5turn);
-
skew kind of stretches the element diagonally
- The x and y axis can be manipulated independently with skewX and skewY
transform: skew(45deg);
-
All the operations above can be applied sequentially to transform with a separating space
- The order in which they're applied can result in different effects
-
The transform operations execute from an anchor called the origin, which is the center by default
- Depending on where the anchor is placed, operations are performed relative to that location
- You can set the origin in multiple ways such as:
- left top
- 25px bottom
- 0% 150%
transform-origin: center;
-
Transforms don't work with inline elements in flow layout
- Use display: inline-block or switch to another layout mode
CSS Transitions
-
The transition property allows changes that occur to smooth out
- Animations without this property would look like they're teleporting
- The browser is instructed to interpolate from one state to another when using transition
button { transition: transform 250ms; } button:hover { transform: translateY(10px); }
-
This property requires two values, the name of the property to animate and the duration of the animation
- A comma-separated list can be used if multiple properties need to be animated
button { transition: transform 250ms, opacity 400ms; } button:hover { transform: translateY(10px); opacity: 0; }
-
Timing functions determine the pace at which an element moves in each frame
- The transition-timing-function property can specify the different timing functions available, or you can use the shorthand
button { transition: transform 250ms; transition-timing-function: linear; }
button { transition: transform 250ms linear; }
-
Types of timing functions:
- linear: Moves at a constant pace
- ease-out: The initial pace is fast but slows down towards the end
- ease-in: Opposite of ease-out, starts slow and then speeds up
- ease-in-out: Combines the other two functions, going slow at the start and end but fast during the middle section. The timing is symmetrical
- ease: Similar to ease-in-out but it's not symmetrical, there's a brief ramp-up with lots of deceleration. This is the default value
-
Custom timing curves can be defined using the cubic-bezier function
- It takes in 4 values which represent the points of the time and state
- The values set are between 0 and 1
- Here are the built-in timings replicated using the cubic-bezier function
/* linear */ transition-timing-function: cubic-bezier(0, 0, 1, 1); /* ease-out */ transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); /* ease-in */ transition-timing-function: cubic-bezier(0.75, 0, 1, 1); /* ease-in-out */ transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1); /* ease */ transition-timing-function: cubic-bezier(0.44, 0.21, 0, 1);
-
transition-delay allows us to maintain the current state for a brief interval
- Useful in nested dropdown menus so users can move their cursor to adjacent dropdowns without it closing immediately
- In the example below, if the user unhovers their cursor from the dropdown wrapper then nothing will happen for 300ms
.dropdown { opacity: 0; transition: opacity 400ms; transition-delay: 300ms; } .wrapper:hover .dropdown { opacity: 1; transition: opacity 100ms; transition-delay: 0ms; }
Keyfame Animations
-
Keyframe animations are declared using the @keyframes at-rule. Transitions between sets of CSS declarations can be specified
- Supposed to be general and reusable
- Each @keyframes statement needs a name
@keyframes slide-in { from { transform: translateX(-100%); } to { transform: translateX(0%); } }
-
Keyframe animations can be applied selectors using the animation property
- It requires the keyframe statement name and the duration of the animation
- The browser interpolates the declarations within the from and to blocks over the specified duration
.box { animation: slide-in 1000ms; }
-
Timing functions can also be applied to animations using the animation-timing-function property
-
Keyframe animations run once by default, but can be looped with the animation-iteration-count property
- Either an integer can be specified or the special value infinite can be used
-
Using percentages to represent progress instead of from and to in the keyframe statement enables us to add multiple steps
@keyframes fancy-spin { 0% { transform: rotate(0turn) scale(1); } 25% { transform: rotate(1turn) scale(1); } 50% { transform: rotate(1turn) scale(1.5); } 75% { transform: rotate(0turn) scale(1.5); } 100% { transform: rotate(0turn) scale(1); } }
-
animation-direction controls the order of the keyframes, the default value is normal
- reverse plays the animation backwards, going from 100% to 0%
- alternate ping-pongs between normal and reverse on subsequent iterations
- Useful for breathing animations which typically have a grow and shrink theme
- Reduces keyframe statement size
-
Similar to transition, the animation shorthand can combine all the mentioned properties
- Since each property accepts different values, the order doesn't matter here
- The exception is animation-delay, another property which must come after duration
- Better to exclude it from the shorthand to increase readability
animation: slide-in 1000ms linear infinite alternate;
Fill Modes
-
Declarations in the from and to blocks only apply while the animation is running
- After the animation ends, the CSS declarations set directly to the element is reapplied
- One way to override this behaviour is to set the declarations in the to block to the element
- In the snippet, the box turns invisible over a second and will REMAIN invisible since opacity: 0 is also set in .box
@keyframes fade-out { from { opacity: 1; } to { opacity: 0; } } .box { animation: fade-out 1000ms; opacity: 0; }
-
However, instead of relying on default declarations, we can use animation-fill-mode instead
- Allows us to persist the final value from the animation, using the forwards value
- Similarly, there may be no initial CSS for an element before an animation occurs
- We can use backwards to apply the animation's initial state backwards in time
- The value both applies forwards and backwards functionality together
- animation-fill-mode can be used in shorthand as well without any conflicts
Dynamic Updates
-
Animations can be set to run if some event occurs, using JavaScript, the animation property can be added dynamically at any time
- The following code renders a button and box
- The button toggles the animation, causing the box to jump up and down, otherwise it remains still
- When the animation property is set to a valid value, the animation begins
function App() { const [animated, setAnimated] = React.useState(false); return ( <Wrapper> <Box style={{animated : "jump 1000ms infinite" : undefined }} ></Box> <button onClick={() => setAnimated(!animated)}></button> </Wrapper> ) }
- The following code renders a button and box
-
"Interruptions" occur when animations are disabled during the animation process, creating a jarring effect since all the CSS is reset
- animation-play-state keeps the animation applied between a running and paused state
- When the animation resumes, it will continue where it left off
- Consider modifying our previous example, when the animation stops now, the box will be mid-air from jumping instead of resetting
<Box style={{animationPlayState: animated ? "running" : "paused" }} ></Box> ... const Box = styled.div` ... animation: jump 1000ms infinite; `;
-
transition is good for smoothening out harsh transitions betwen values or changing CSS as a result of application state or user action
-
There are some things only @keyframes can do compared to transition without relying on JavaScript
- Looped animations
- Multi-step animations
- Pauseable animations
With styled-components
- The @keyframes at-rule should be declared globally, this can be achieved with styled-components by importing their package
- The keyframes function is called using tagged template literals, just like styled helper functions
- The syntax to define an animation is the same as vanilla CSS
import styled, { keyframes } from "styled-components"; const float = keyframes` ... `; const FloatingCircle = styled.div` animation: ${float} 1000ms infinite alternate ease-in-out; `
Animation performance
-
The pixel pipeline:
- Recalculating style: Figure out which CSS declarations apply to which elements
- Layout: Figure out where each element sits on the page
- Paint: Paint elements, process of figuring out the colour of every pixel (rasterisation) and filling it in
- Compositing: Transform previously-painted elements
-
Different CSS properties trigger different steps in the pixel pipeline
- Try avoid properties affecting layout such as width, height, padding and margin, it's slower
- Animating properties relating to colours are faster because they don't affect layout, starting from the painting step
- transform and opacity can be animated with just the compositing step alone, but there is support for other properties depeneding on browser
-
Hardware acceleration is the process of transferring the optimisation process from the browser (CPU) to the GPU
- The GPU receives everything as a texture, and is good at texture-based transformations which create a performant animation
- GPUs and CPUs renders a bit differently, so there may be a slight shifting of elements
- The will-change property allows us to be intentional about which elements are handled by the GPU (which elements are hardware-accelerated)
button { will-change: transform; }
Designing Animations
-
Types of animation
- Transitions: Change content significantly, such as moving positions
- Supplements: Add/remove information from the page without changing their location
- Feedback: Helps user understand how the application responds to user input
- Demonstrations: Education purpose, visual way of showing how something works
- Decorations: Aesthetics, don't affect information on the page
-
Animation duration needs to be specified in milliseconds
-
Most animations generally use a duration between 200ms to 500ms
Action-Driven Animation
-
Use CSS transitions to animate state
- Modals are either opened or closed for example
-
Animations based off pseudo-selectors like :hover can be achieved by using different transition values
button { transition: transform 500ms; /* Exit */ } button:hover { transform: scale(1.1); transition: transform 150ms; /* Enter */ }
-
Can dynamically change duration using JavaScript and React with styled-components
Orchestration
-
Improve animation by sequencing it, orchestrating different elements
-
Implementing orchestration essentially requires ternaries
-
Example of one styled component:
<Backdrop style={{ opacity: isOpen ? 0.75 : 0, transition: 'opacity', transitionDuration: isOpen ? '1000ms' : '500ms', transitionDelay: isOpen ? '0ms' : '100ms', transitionTimingFunction: isOpen ? 'ease-out' : 'ease-in', }} onClick={handleDismiss} />
- Can extract the animation settings from the overall structure using a helper function
function getTransitionStyles(isOpen) { /* Return 3 objects, one for each animated element */ } const { backdropStyles, modalStyles, closeButtonStyles } = getTransitionStyles(isOpen); return ( <Wrapper> <Backdrop style={{ opacity: isOpen ? 0.75 : 0, ...backdropStyles, }} onClick={handleDismiss} /> ... </Wrapper> )
Accessibility
-
Animations enhance user experience, but certain people feel nausea or dizziness for example
-
Modern operating systems allow users to opt out of animations
- Google "reduce animations" and mention the specific operating system
-
Apple first added the prefers-reduced-motion media query for Safari, since then other browsers and OS have followed suit
-
Accessing media query using CSS:
- reduce will be set if the user clicks the "reduce animations" checkbox
- no-preference is the default value
button { transition: transform 500ms; /* Exit */ } button:hover { transform: scale(1.1); transition: transform 150ms; /* Enter */ } @media (prefers-reduced-motion: reduce) { button { transition: none; } }
-
Accessing media query in JavaScript:
- Can also use event listeners to update the value
function getPrefersReducedMotion() { const mediaQueryList = window.matchMedia('(prefers-reduced-motion: no-preference)'); const prefersReducedMotion = !mediaQueryList.matches; return prefersReducedMotion; }
-
Can also create a custom hook in React to perform the same functionality
-
Different people have different tolerances for the acceptable level of motion
- Small colour changes, small fade-in-outs or minor pixel movements can remain unchanged
- Large sweeping motions or opacity changes should be disabled
3D Transforms
-
There is no perspective in isometric projection, everything appears at the same scale
-
Perspective projection mimics real life, where things vanish into the distance
-
The 3D engine in CSS uses isometric projection by default, but the it can be switched with the perspective property
- Elements appear 3D, further elements look smaller and vice versa, or which edge is closer/further away from us
- The perspective property must be on the parent element
- Supply a length value to switch to perspective projection
- The value is a measure of how close the user is to the screen, small changes appear huge
-
Another way to apply perspective projection is to use the perspective function
- Applied to each element rather than the parent
.box { transform: perspective(250px) rotateX(45deg); }
Rendering Contexts
-
If applying a rotating transition on sibling elements, they may overlap
- Use z-index on each element
-
Alternatively, use transform-style property with the preserve-3d value
- Instead of using stacking contexts and z-index layers, elements are positioned in 3D space
-
transform-style: preserve-3d creates a 3D rendering context
- Allows elements to show up "in front"
- Allows elements to intersect
Gotchas
-
When applying perspective to descendants, it can trigger a "doom flicker"
- When an element is hovered, it causes a rotation
- If the cursor is near the BottleRocketImage, it will rotate away from the cursor which means it's no longer hovered
- Essentially, the element flickers between hover and non-hover states
-
To resolve "doom flicker", it needs three things
- A shared parent that sets perspective
- A wrapper over each individual element that listens for hover/focus
- A child that applies the transition when its wrapper is hovered/focus
- transform-style: preserve-3d must be applied to .card-link now since .wrapper and .card are no longer in a direct parent-child relationship
- When .wrapper sets it, a new context is not created, but rather it forwards the context downwards to allow grandchildren to participate
- Acting as a bridge, it can be applied to as many descendants as long as each layer repeats this trick
/* 1. Shared parent */ .wrapper { perspective: 500px; transform-style: preserve-3d; } /* 2. Individual card wrapper */ .card-link { display: block; transform-style: preserve-3d; } /* 3. Child to be transformed: */ .card { transform-origin: top center; will-change: transform; transform: rotateX(0deg); transition: transform 750ms; } /* Apply the transforms to the childwhen we hover/focus the wrapper: */ .card-link:hover .card, .card-link:focus .card { transform: rotateX(-35deg); transition: transform 250ms; }
-
Scrollburglars can appear when hovering over some transformed element
-
Can apply overflow: hidden but not on the parent element which creates the 3D rendering context
- .wrapper is "fragile", there is a list of properties that can disable the 3D rendering context
- overflow, clip-path, opacity, filter, mix-blend-mode
- Need to apply the property to a new parent element, .outer-wrapper
.outer-wrapper { overflow: hidden; } .wrapper { perspective: 500px; transform-style: preserve-3d; }
- .wrapper is "fragile", there is a list of properties that can disable the 3D rendering context
Ecosystem World Tour
-
The web animations API is a low-level animation API built into the browser
-
Mirrors the @keyframes API
const elem = document.querySelector('.box'); const frames = [ { opacity: 0, transform: 'translateY(100%)' }, { opacity: 1, transform: 'translateY(0%)' }, ]; elem.animate( frames, { duration: 1000, iterations: Infinity, } );
-
Pros:
- Built into the CfnBrowserSettings, so no large package needs to be included in bundle
- Good performance, won't be affected by any work happening in the JS main thread
- It has solid browser support
- It allows more customisation compared to CSS keyframe animations
-
Cons:
- Not fundamentally different from CSS keyframe animations, nothing new added
- Subtle differences between keyframe animations and the web animations API, such as default timing function is linear instead of ease
-
Framer Motion is a production-ready library for React
- Can animate CSS properties that can't normally be animated
- Works by calculating a complex sequence of transforms required to interpolate between the start and end positions
- Can even animate transitions between components
-
Pros:
- Can animate all kinds of things that would normally not be animatable
- Uses hardware-accelerated transforms for performant transitions
- Supports using spring physics instead of Bezier curves
- Works seamlessly with React, can add sophisticated animations in small amounts of code
-
Cons:
- Package weighs about 32kb gzip
- Like all JS-based animation libraries, the animation might run choppy if the main thread becomes occupied
- Only works with React
-
React Spring enables us to model animations based on spring physics, rather than Bezier curves
-
Fundamentally, React Spring is a number generator
- This means it can be used for all types of animations, not just transitioning CSS properties
-
Pros:
- Fluid, organic motion compared with CSS transitions / keyframe animations
- Highly optimised performance
- Relatively small, about 18kb gzip
- A rich API with lots of advanced options, including orchestration tools
- Ties in with an ecosystem, can use react-use-gesture to create the card-grabbing animation
-
Cons:
- Can't do "magic" animations like Framer Motion, though, Framer Motion supports spring physics
- Steep learning curve
- Like all JS-based animation libraries, the animation might run choppy if the main thread becomes occupied
-
GreenSack GSAP is an old animation library
-
Offers advanced Bezier-based easing and a timeline to manage orchestration
-
Pros:
- Large, active community
- Can be used with any framework
- Highly optimised performance
- Rich plugin ecosystem
-
Cons:
- Doesn't support true spring physics, but can emulate it more accurately than CSS transitions
- Not framework-agnostic, won't tie in as neatly as framework-specific solutions