- INDEX
- Guidance and clarification
- Draw attention to specific elements
- Provide visual feedback for user interactions (especially in mobile devices when tapping)
- Style and branding
- Convey personality and tone
- Create a memorable user experience
- Enhance storytelling and engagement
-
Duration
- How long an iteration of an animation takes to complete
- It applies to each iteration, not the entire animation
-
Delay
- How long to wait before starting the animation
- It only happens once, at the start of the animation (the first iteration)
-
Timing Function
-
Iteration Count
- How many times the animation should repeat
-
Direction
- The direction the animation should play (e.g., normal, reverse, alternate)
-
Fill Mode
- What styles are applied before and after the animation (e.g., forwards, backwards)
-
CSS Variables
- They can be used to create dynamic animations by changing their values in different states or with JavaScript
:root { --duration: 2s; } .box { animation-duration: var(--duration, 1s); /* fallback to 1s if --duration is not defined */ }
const box = document.querySelector('.box'); box.style.setProperty('--duration', '5s'); // change duration to 5s
It allows us to change a specified element in some way. It comes with a grab-bag of different transform functions that allow us to move and contort our elements in many different ways.
- It's the best way to change element properties when animating elements in CSS, because it's more efficient as it doesn't trigger layout or paint
-
translate()-> it moves the element from its current position
- We can use it to shift an item along in either axis:
xmoves side to side,ymoves up and down. Positive values move down and to the right. Negative values move up and to the left.
Critically, the item's in-flow position doesn't change. As far as our layout algorithms are concerned, from Flow to Flexbox to Grid, this property has no effect.
For example: If we have 3 flex items next to each other aligned using Flexbox. When we apply a
transformto the middle child, the Flexbox algorithm doesn't notice, and keeps the other children in the same place. This is similar to howtop/left/right/bottomwork in positioned layout, with relatively-positioned elements.- Powerfull thing: When we use a percentage value in translate, that percentage refers to the element's own size, instead of the available space in the parent container.
- Setting transform:
translateY(-100%)moves the box up by its exact height, no matter what that height is, to the pixel. - We can also use the magic of
calc()to do more complex things. For example,transform: translateY(calc(-100% - 10px))moves the box up by its height, plus10pixels.
- Setting transform:
- We can use it to shift an item along in either axis:
-
scale()-> it scales the element (grow or shrink)
scaleuses a unitless value that represents a multiple, similar toline-height.scale(2)means that the element should be2xas big as it would normally be.
- We can also pass multiple values, to scale the x and y axis independently:
scale(2, 0.5)would make the element twice as wide and half as tall. - What is the difference between
widthandscale?widthchanges the size of the element, but it doesn't change the text size inside it and it affects the layout of the page.- Because it affects the layout, it can trigger a reflow, which is expensive.
scalechanges the size of the element and everything inside it, including text size, and it doesn't affect the layout of the page.- Because it doesn't affect the layout, it's much cheaper. That's why it's the best way to animate elements in CSS.
- Note: This is an advanced technique, But know that it's possible to use scale to increase an element's size without distorting its children. Libraries like Framer Motion take advantage of this fact to build highly-performant animations without stretching or squashing.
-
rotate()-> it rotates the element
- We typically use the
degunit for rotation, short for degrees. But there's another handy unit we can use, one which might be easier to reason about:turn.1turnis a full rotation,0.5turnis a half rotation, and so on.rotate(0.5turn)is the same asrotate(180deg).
- We typically use the
-
skew()-> it skews the element along the X and Y-axis
- It uses the
degunit, just likerotate. skew(30deg, 20deg)would skew the element 30 degrees along the x-axis and 20 degrees along the y-axis.
- It uses the
-
transform-origin-> it changes the origin of the transform
- The
transform-originproperty allows us to change the point around which the element is transformed. - It acts as a pivot point.
- By default, the origin is the center of the element.
- We can use keywords like
top,bottom,left,right,center, or we can use a length value like50px,10%, etc. - We can also use a combination of keywords and length values, like
top right,center bottom, etc. - Note: The
transform-originproperty is a shorthand fortransform-origin-xandtransform-origin-y. If you only specify one value, it will be used for both x and y.
- The
-
perspective-> it changes the perspective of the 3D element.box { transform: perspective(100px) rotateY(45deg); }
- It's used to give a 3D effect to the element.
- It's more common to use the
perspectiveproperty on the parent element, not the child element. and use it instead of theperspectivefunction in thetransformproperty.- Example here: Rotating elements (3D perspective)
-
transform-style-> it defines how nested elements are (rendered and positioned) in 3D space-
This property allows us to opt in to a genuine 3D engine. We can do cool stuff not possible with
z-index:.box { transform-style: preserve-3d; }
-
flat-> default -> it's like 2D, it doesn't render the nested elements in 3D spacepreserve-3d-> it renders the nested elements in 3D space
-
When we apply
transform-style: preserve-3d, we create a 3D rendering context. This is similar in philosophy to a stacking context.- When we create a 3D rendering context, we allow all descendants to be positioned in 3D space. They'll grow bigger as they approach the user. They'll be allowed to intersect, like the shapes above.
- Like stacking contexts, the context will apply all the way down the tree, not just to direct children.
-
With
z-index, we're assigning each element a layer. Withpreserve-3d, we're positioning elements in 3D space, and letting the 3D engine figure out the layering.
-
-
Notes:
-
Combining multiple operations
.box { transform: translateX(50px) rotate(45deg) scale(1.5); }
transformis a shorthand property that combines multiple transform functions into one.- The order of the functions in the
transformproperty matters. The functions are applied in the order they're written.- The transform functions are applied from right to left, like composition in functional programming.
- (rotating then scaling) is different from (scaling then rotating).
- We can use multiple transform functions in the same
transformproperty, separated by spaces.
-
The
transformproperty is a shorthand fortransform-function,transform-origin, andtransform-style. -
The percentages in
translateandscaleare relative to the element's size, not the parent's size. -
Gotcha:
transformdoesn't work with inline elements in Flow layout.- You need to change the display property to
inline-blockorblockto make it work.
- You need to change the display property to
-
To generate 3D transform -> CSS 3D Transform Generator
transition property is used to animate the changes in css properties when their state changes (e.g. hover, focus, active, etc.)
-
It works by applying the changes in the css properties over a period of time instead of instantly when the state changes.
-
The
transitionshorthand property consists of multiple properties:
- Only 2 properties are required:
propertyandduration
- Only 2 properties are required:
-
Here're the properties for
transitionseparately:-
transition-property-> it's the css-properties that will be animated -
transition-duration-> it's the time the transition takes- must specify the unit (
s), even it's zero seconds
- must specify the unit (
-
transition-timing-function-> it's how the transition takes place (rate of change)
linear= same speed start to end- 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!
ease- default = slow start, fast, slow end- It's similar to
ease-in-out, but it's a bit more extreme. - is a great option in most cases. Unless you're specifically going for a different effect, ease makes a lot of sense. That's why it's the default.
- It's similar to
ease-in= slow start- It's most commonly used when something is leaving the screen (eg. a modal disappearing). It produces the effect that something is moving away from the user, and then slows down as it disappears.
ease-out= slow end- 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-out= slow start, fast, slow end- It's a combination of
ease-inandease-out. - It's most commonly used when something is moving across the screen. It produces the effect that something is moving towards the user, then speeds up as it passes by, and then slows down as it leaves. (eg. element fading in and out over and over)
- It's a combination of
-
transition-delay-> it's the time before the transition starts (postponing the transition)- must specify the unit (
s), even it's zero seconds - It's recommended to use it separately from the
transitionshorthand property, because it's not used frequently and it's more readable when it's separated
- must specify the unit (
-
-
If you have multiple different properties and want to make their transition different, you can:
-
Declare each on in the block where it happens
button { opacity: 0.5; } button:active { transition-duration: 0.5; opacity: 1; }
-
Or, declare multiple values in the
transition-propertyseparated by commabutton { transition-property: background-color, border-radius; transition-duration: 4s, 2s; } button:hover { background-color: red; border-radius: 50%; }
-
-
If you plan on animating multiple properties with the shorthand property, you can pass it a comma-separated list:
button { transition: background-color 1s, border-radius 2s; }
-
Notes:
-
transition-propertytakes a special value:all. Whenallis specified, any CSS property that changes will be transitioned. It can be tempting to use this value, as it saves us a good chunk of typing if we're animating multiple properties, but It's not recommended⚠️ .- This is because it can lead to unexpected behavior. If you add a new property to the element, it will be transitioned as well, which might not be what you want.
- At some point in the future, you (or someone on your team) will change this CSS. You might add a new declaration that you don't want to transition. It's better to be specific, and avoid any unintended animations. (Animation is like salt: too much of it spoils the dish.)
- Also it can lead to performance issues, as there might be "layout properties" that are expensive to animate.
- This is because it can lead to unexpected behavior. If you add a new property to the element, it will be transitioned as well, which might not be what you want.
-
Custom timing function curve
-
You can create your own timing function curve using
cubic-bezierfunctionbutton { transition: background-color 1s cubic-bezier(0.17, 0.67, 0.83, 0.67); }
-
You can use online tools to generate the cubic-bezier curve, like cubic-bezier.com

-
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.

-
-
Common
transition-delayissue:-
Have you ever tried to mouse over a nested navigation menu, only to have it close before you get there?

-
As a JS developer, you can probably work out why this happens: the dropdown only stays open while being hovered! As we move the mouse diagonally to select a child, our cursor dips out of bounds, and the menu closes.
-
The solution is to add a delay to the transition, so that the menu doesn't close immediately when the cursor leaves the parent. This gives the user a chance to move their cursor to the child menu.
.dropdown { opacity: 0; transition: opacity 400ms; transition-delay: 300ms; } .dropdown-wrapper:hover .dropdown { opacity: 1; transition: opacity 100ms; transition-delay: 0ms; }
-
This way, the dropdown will stay open for 300ms after the cursor leaves the parent, giving the user a chance to move their cursor to the child menu.
-
-
"Doom flicker" issue
-
It's a common animation bug where an element stutters up and down quickly in an unintentional, unpleasant way:

-
The issue is that the
transitionproperty is applied to the element when it's first rendered, and then removed when the element is removed from the DOM. This causes the element to animate back to its original state before it's removed. -
To solve this problem, we separate the trigger from the effect. We listen for hovers on the parent
<button>, but apply the transformation to a child element. This ensures that the hover target won't move out from under the cursor.<!-- Before ❌ --> <style> .btn { width: 100px; height: 100px; border: none; border-radius: 50%; background: slateblue; color: white; font-size: 20px; font-weight: 500; line-height: 1; transition: transform 250ms; } .btn:hover { transform: translateY(-10px); } </style> <button class="btn">Hello World</button> <!-- After ✅ --> <style> .btn { width: 100px; height: 100px; border: none; background: transparent; padding: 0; } .btn:hover .btn-contents { // 👈 apply the transform to the content when the wrapper is hovered transform: translateY(-10px); } .btn-contents { display: grid; place-content: center; height: 100%; border-radius: 50%; background: slateblue; color: white; font-size: 20px; font-weight: 500; line-height: 1; transition: transform 250ms; } </style> <button class="btn"> <span class="btn-contents">Hello World</span> </button>
- This works because hover states bubble up, just like
mouseEnterevents in JavaScript. When we hover over.btn-contents, we're also hovering over all of its ancestors (.btn,body, etc).
- This works because hover states bubble up, just like
-
-
-
You can't use
backgroundwithtransitionproperty, so if you want to animate the background you can either:-
use
background-colororbackground-imageinstead. -
use
box-shadowwithinsetinstead:button { transition: 1s; } button:hover { box-shadow: 0 0 0 2em red inset; }
-
CSS keyframe animations are declared using the @keyframes at-rule. We can specify a transition from one set of CSS declarations to another
Think of it as a Timeline for an animation, where we can define multiple points in time (keyframes) and the styles that should be applied at those points.
-
Applying animation to an element is done by 2 steps:
-
Define the animation with a name ->
@keyframeskeyframesis a rule which allows you to define an animation sequence with multiple steps
@keyframes move { from { transform: translateX(20px); } to { transform: translateX(100px); } }
-
Apply the animation ->
animation-nameand propertiesdiv { animation-name: move; animation-duration: 10s; animation-iteration-count: 3; /* or using the shorthand */ animation: move 10s infinite; }
-
-
The browser will interpolate the declarations within our
fromandtoblocks, over the duration specified. This happens immediately, as soon as the property is set. -
Keyframe animations are meant to be general and reusable. We can apply them to specific selectors with the animation property and not just one element.
animation property consists of multiple properties:
-
animation-name -
animation-duration -
animation-delay- It's the time before the animation starts (for each iteration)
-
animation-iteration-count-
It's the number of times the animation will run
-
1-> it will run once -
2-> it will run twice -
n-> it will runntimes -
infinite-> it will run forever (common for loading-state animations)Note that for loading-spinners, we want to use a
lineartiming function, so that the spinner doesn't speed up or slow down as it spins.
-
-
-
animation-direction- It's the direction of the animation, it controls the order of the keyframes (from start to end)
normal-> default -> from0%to100%reverse-> from100%to0%(Backwards)alternate-> from0%to100%then from100%to0%alternate-reverse-> from100%to0%then from0%to100%
- It's the direction of the animation, it controls the order of the keyframes (from start to end)
-
animation-timing-function -
animation-fill-mode-
it's what happens when the animation finishes or before it starts (before the first iteration)

-
none-> default -> go to initial state (0%) before start and go to final state (100%) after finish
- Technically it doesn't go to the
0%state, it just doesn't apply the styles from the keyframes and goes to the initial state (the state before the animation)
- Technically it doesn't go to the
-
forwards-> stick to final state (100%) after finish (moving forward)
-
backwards-> default -> go to initial state (0%) after finish (moving backward)
- It's to copy all of the declarations in the
fromblock and apply them to the element ASAP, before the animation has started. - Useful when we have
animation-delayand we want to apply the styles before the animation starts
- It's to copy all of the declarations in the
-
both->forwards+backwards-> stick to final state (100%) + go to initial state (0%) after finish
- What if we want to persist the animation
forwardsandbackwards? We can use a third value, both, which persists in both directions - Useful when we want to apply the styles before the animation starts and after it finishes (e.g. delay and persist the final state)
- What if we want to persist the animation
-
-
-
animation-play-state-
it's to pause and resume the animation

paused-> pause the animationrunning-> resume the animation
-
Example: pause the animation on hover
div { animation: move 2s infinite; animation-play-state: running; } div:hover { animation-play-state: paused; // this is better than using `animation: none;` because it doesn't cause interruption }
-
They're used to define the animation states and the time between them (the animation steps)

-
keyframesare different fromtransitionas they're not just the start and end points, but they're the steps between them -
keyframescan be defined in percentage%or byfromandto:div { animation-name: move; animation-duration: 10s; animation-iteration-count: 3; /* or using the shorthand */ animation: move 10s infinite; } @keyframes move { 0% { transform: translateX(20px); } 50% { transform: translateX(100px); background: red; } 75% { transform: translateX(-200px); background: yellow; } 100% { transform: translateX(20px); background: green; } } /* or when we have one start point and one end point */ @keyframes move { from { transform: translateX(20px); } to { transform: translateX(100px); background: red; } }
-
If you have multiple keyframes steps that have the same properties, you can combine them using comma (
,):@keyframes move { 0%, 100% { transform: translateX(20px); } 50% { transform: translateX(100px); } }
-
Alternating animations
-
It's when you want to animate an element back and forth between two states
-
It can be done using 2 approaches:
-
Using odd number of keyframes (minimum 3 keyframes)
@keyframes move { 0% { transform: scale(1); } 50% { transform: scale(1.5); } 100% { transform: scale(1); } }
-
Using
alternatevalue inanimation-directionpropertydiv { animation: move 2s infinite alternate; } @keyframes move { 0% { transform: scale(1); } 100% { transform: scale(1.5); } }
-
-
When animating multiple elements, we often want to stagger their animations, so that they don't all start at the same time. This can create a more interesting and dynamic effect.

it's to use the nth-child selector and CSS variables to choreograph animations between multiple elements.
-
Normal Example
.balls-container { --duration: 1s; animation: move-right var(--duration) both; &:nth-child(2) { animation-delay: calc(var(--duration) - 0.1s); } &:nth-child(3) { animation-delay: calc(var(--duration) * 2 - 0.1s * 2); } }
-
Example using React or a framework:
// Here, we can pass the index of the element as a css variable // and use it to calculate the animation delay const balls = [1, 2, 3, 4, 5]; return ( <div className='balls-container'> {balls.map((ball, index) => ( <div key={ball} className='ball' style={{ '--i': index }} // passing the index as a CSS variable ></div> ))} </div> );
.balls-container { --duration: 1s; .ball { animation: move-right var(--duration) both; animation-delay: calc(var(--i) * var(--duration) - 0.1s * var(--i)); } }
So far, all of the examples we've seen involve an animation running right on page load (or after a prescribed delay).
That's not quite the right way to think about it though. There's no rule that says that animations need to happen immediately! We can start and stop them whenever we want, and even change their properties on the fly.
-
It's more accurate to say that the animation will start as soon as a valid animation is wired up, using the
animationproperty. Using JavaScript, we can add that property dynamically, at any point in time:<style> .box { width: 100px; height: 100px; background: red; animation: move 2s infinite; } @keyframes move { 0% { transform: translateX(0); } 100% { transform: translateX(100px); } } </style> <div class="box"></div> <script> const box = document.querySelector('.box'); box.addEventListener('click', () => { box.style.animation = 'undefined'; // stop the animation box.style.animation = 'move 2s infinite'; // start / restart the animation }); </script>
- When the page loads, the
animationproperty is set toundefined, and so nothing happens. When the user clicks the box, theanimationproperty is set tomove 2s infinite, and the animation starts. - To stop the animation, we set the
animationproperty toundefined. This is a bit of a hack, but it works. The browser will stop the animation immediately, and the element will revert back to its default CSS.
- When the page loads, the
-
Notes:
- When we remove the
animationproperty, all of the CSS in the from and to blocks evaporates immediately. The element will revert back to its default CSS.- This is known as an “interruption”.
@keyframesanimations don't handle interruptions well. - There is a tool that can help in certain situations, though:
animation-play-state. This property can pause and resume animations, and it has excelent browser support.
- This is known as an “interruption”.
- When we remove the
You can use data-state attribute to define state and make css values established based on current data-state
-
you change data.state in Javascript which then reflects in the
data-stateHTML attribute, then define which css property will be shown<div class="container" data-state="success"></div> <div class="container" data-state="loading"></div> <div class="container" data-state="error"></div>
.container[data-state='success'] { animation: slide-up 1s both; } .container[data-state='loading'] { animation: pulse 1s infinite; } .container[data-state='error'] { animation: shake 0.5s both; }
-
We can use Javascript to change the
data-stateattribute, which will trigger the corresponding animation:const container = document.querySelector('.container'); // Change state to loading container.setAttribute('data-state', 'loading'); // After some time, change state to success or a HTTP response setTimeout(() => { container.setAttribute('data-state', 'success'); }, 3000);
-
Note: we can also use
classinstead ofdata-state, but usingdata-stateis more semantic and easier to understand the purpose of the attribute.// still works .container.success { animation: slide-up 1s both; } .container.loading { animation: pulse 1s infinite; } .container.error { animation: shake 0.5s both; }
-
Examples:
-
The timing function applies to each step. We don't get a single ease for the entire animation
-
animation-fill-modeapplies to the first iteration of the animation, not the last. If you want the final state to stick, you need to useforwardsorboth. -
Note that when using the
animation-directionproperty withalternateoralternate-reverse, these things will happen:- The animation will be done in half the time because it's going from
0%to100%then from100%to0%, and the time is shared between them - the
animation-fill-modeproperty will apply to both the forwards and backwards animations.
- The animation will be done in half the time because it's going from
-
animationshorthand property can be used to define all the animation properties in one linediv { /* animation: name duration timing-function delay iteration-count direction fill-mode play-state; */ animation: move 10s infinite; }
-
Here's a piece of good news, as well: the order doesn't matter. For the most part, you can toss these properties in any order you want.
.box { /* This works: */ animation: grow-and-shrink 2000ms ease-in-out infinite alternate; /* This also works! */ animation: grow-and-shrink alternate infinite 2000ms ease-in-out; }
-
This works because different properties accept different values;
alternate, for example, isn't a validtiming-functionoriteration-count, so the browser can deduce that you mean to assign it toanimation-direction. -
There is an exception:
animation-delayproperty needs to come after theduration, since both properties take the same value type (milliseconds/seconds).-
For that reason, It's prefered to exclude delay from the shorthand:
.box { animation: grow-and-shrink 2000ms ease-in-out infinite alternate; animation-delay: 500ms; }
-
-
-
-
We can access scoped CSS-Variables defined in the selector -> inside the
@keyframesblock@keyframes move { 0% { transform: translateX(var(--start)); } 100% { transform: translateX(var(--end)); } } .circle { --start: 0; --end: 100px; animation: move 2s infinite; }
- This is because the
@keyframesblock is a child of the selector (where the animation is used), and so it can access the CSS variables defined in the selector.
- This is because the
The main differences between transition and animation:
| Transition | Animation |
|---|---|
| Simple animations between 2 states (like hover effects) | Complex animations with multiple states (not just start and end states, but in-between) |
| Changes happen between start and end state | Can define multiple steps with keyframes |
| Can't be reused easily | Can be reused across different elements |
Key points:
transitionis best for state changes (like hover effects)animationis better for:- Looping animations
- Multi-step animations
- Animations that can be paused
- Animations that start when page loads
Choose transition for simple state changes, and animation for more complex, repeating, or automatic animations.
Performance is important when is comes to animation, Sluggish animations can ruin an otherwise good user experience.
The tolerances are also really tight. In order for our brain to perceive motion as fluid and believable, it needs to run at 60 frames per second: this leaves us with only ~16 milliseconds to update each frame!
Animation in an expensive process of CPU/GPU and specially on CPU, and it's related to how the browser-rendering-engine works:

-
If we want to update the colors of the pixels on our screen, there's a pipeline of possible steps (pixel pipeline):
-
Recalculate styles
- it figures out which CSS rules apply to which elements
- it's the most expensive step, because it requires the browser to recalculate the layout of the page
-
Layout
- it figures out where things are on the screen
-
Paint
- once we know where everything is, we can start painting them. This is the process of figuring out which color every pixel should be (“rasterization”), and filling it in.
- it fills in the pixels
-
Composite
- it blends all the layers together to create the final version of the page
- it's the cheapest and most desirable for high pressure points in an app's lifecycle, like
animationsorscrolling - It lets the browser re-use the work done in previous frames.
- It was invented to help with scroll performance. In the early days of the web, the entire page had to be repainted on every frame when the user scrolled. This was slow and miserable, so the smart folks who work on browsers found a way to skip the paint process, and instead slide the page's content up or down when the user scrolls.
- Compositing is lightning-quick because it doesn't have to do many calculations. It's all about transforming the stuff it has already calculated (sliding it around, rotating it, etc).
-
-
Different CSS properties will trigger different steps in the pixel pipeline. If we animate an element's
height, we'll need to recalculate the layout, since an item shrinking might mean that its siblings scoot up to fill the space.
Here are some guidelines (CSS triggers):
-
Composite ✅ -> It's for blending things together like with:
transform,opacity- If you change a property that requires neither layout nor paint, and the browser jumps to just do compositing.
- This final version is the cheapest and most desirable for high pressure points in an app's lifecycle, like
animationsorscrolling.
You can find more here: Stick to Compositor
-
Painting 🤞 ->
color,background -
Layouts ❌ -> one that changes an element’s geometry, like its
height,width,left,right,margin,paddingetc (things that trigger layouts)
Based on their performance impact on the pixel pipeline, here are the CSS properties grouped by their operation type:
Composite Operations (Best Performance) ✅
transform->translate,rotate,scale,skewopacity
Paint Operations (Medium Performance) 🤞
border-radiusbackgroundcolor-> changing the color will never affect the layout as it doesn't change the element's position on the pagebox-shadow
Layout Operations (Worst Performance) ❌
width/heightpositionmargin/padding
Pro tip: For optimal animation performance, stick to composite operations (
transformandopacity) whenever possible.
Does that mean that you can only ever animate transform and opacity?
- Personally, I think we can be a little bit more flexible than that. Not all repaints / layout-recalculations are created equal! For example, tweaking the height of an absolutely-positioned element tends to be quicker, since there's no chance that it will cause siblings to be shifted.
Depending on your browser and OS, you may occasionally notice a curious stutter on certain animations, This happens because of a hand-off between the computer's CPU and GPU.
-
When we animate an element using
transformandopacity, 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:
.box { will-change: transform; }
will-changeis a CSS property that lets the browser know that an element is likely to be animated in the future. This allows the browser to prepare for the change, and optimize the animation (by rendering it on the GPU from the start).- By rendering the animation on the GPU from the start, we avoid the CPU/GPU hand-off, and the animation will be smoother because GPU is better at handling animations.
- It's a way to hint to the browser that it should prepare for the change. It's like saying, “Hey, I'm going to animate this thing, so you should get ready for it.”
- 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”.
-
Tradeoffs
- Nothing in life comes free, and hardware acceleration is no exception. By delegating an element's rendering to the GPU, it'll consume more video memory, a resource that can be limited, especially on lower-end mobile devices.
- This isn't as big a deal as it used to be — There're some testing on a Xiaomi Redmi 7A, a popular budget smartphone in India, and it seems to hold up just fine. Just don't broadly apply
will-changeto elements that won't move. Be intentional about where you use it. - But be careful, and don't use it on a lot of elements. It's a tool to be used judiciously, and with care.
- This isn't as big a deal as it used to be — There're some testing on a Xiaomi Redmi 7A, a popular budget smartphone in India, and it seems to hold up just fine. Just don't broadly apply
- Nothing in life comes free, and hardware acceleration is no exception. By delegating an element's rendering to the GPU, it'll consume more video memory, a resource that can be limited, especially on lower-end mobile devices.
-
But when to use
will-change?- It's best to use
will-changewhen you know that an element is going to change in the future. If you're animating an element on hover, for example, you can addwill-change: transformto the hover state. - When you're animating properties that aren't
transformoropacity, you can usewill-changeto hint to the browser that it should prepare for the change. - It's not a magic bullet, and it's not a property that you should apply to every element on your page. It's a tool to be used judiciously, and with care.
- It's best to use
It's a technique for animating between two different layouts. It works by capturing the initial and final states of the elements, and then animating between them.
It's a technique because it's always hard to animate layout changes, because they often involve changing the size and position of elements on the page. The Flip technique is a way to make these animations smoother and more natural.
-
The Flip technique involves 4 steps:
- First: Capture the initial state of the elements (before the layout change)
- Last: Capture the final state of the elements (after the layout change)
- Invert: Apply a transform to each element that will move it from its initial position to its final position
- Play: Animate the transform back to
none, which will move the element to its final position
-
More here: Animating Layouts with the FLIP Technique
-
For opening and closing a modal, we usually have different settings for each state:
- Opening:
- enter duration ->
500ms - enter timing function ->
ease-out
- enter duration ->
- Closing:
- exit duration ->
300ms - exit timing function ->
ease-in
- exit duration ->
- Opening:
But how can we have multiple settings for the same animation?
-
If the animation is based on a pseudo-selector like
:hover, we can do it by using differenttransitionvalues:<style> .button { /* Exit animations */ transition: transform 500ms; } .button:hover { transform: scale(1.1); /* Enter animation */ transition: transform 150ms; } </style> <button class="button">Hello World</button>
- When the mouse is resting atop the element, the :hover declarations apply, and so the enter animation will be given
transition: transform 150ms. The moment the mouse leaves the element, though, it falls back to the defaulttransition: transform 500ms, and uses that transition for the exit animation.
- When the mouse is resting atop the element, the :hover declarations apply, and so the enter animation will be given
-
If the animation is based on a class or action (keyframes animation), then we can use Javascript to change the duration dynamically:
const ENTER_DURATION = '500ms'; const EXIT_DURATION = '250ms'; const ENTER_EASE = 'ease-out'; const EXIT_EASE = 'ease-in'; function Modal({ isOpen, children }) { return ( <Wrapper style={{ '--transition-duration': isOpen ? ENTER_DURATION : EXIT_DURATION, '--timing-function': isOpen ? ENTER_EASE : EXIT_EASE }}> <DialogContent>{children}</DialogContent> </Wrapper> ); } const Wrapper = styled(DialogOverlay)` transition: transform var(--transition-duration) var(--timing-function); `;
Full details here: Building a Magical 3D Button
- When I see this effect implemented online, I typically see people use
bordersorbox-shadows. These implementations are fine for static buttons, but if we want to animate it, we'll have a much smoother effect if we stick with transforms.
💡 Instead, we'll separate the button into 2 layers: a light-colored front, and a dark-colored back. The front layer will move up and down.
-
Below, we've implemented the broad strokes of this effect, but with no transitions! Update the code so that there are distinct, action-based animations for the following 3 actions:
- Hovering
- Clicking
- Leaving (moving the mouse away from the button)
-
CSS Solution
<style> .front { transform: translateY(-4px); transition: transform 500ms; } .pushable:hover .front { transform: translateY(-6px); transition: transform 250ms; } .pushable:active .front { transform: translateY(-2px); transition: transform 50ms; } </style> <button class="pushable"> <span class="front">Push me</span> </button>
-
Also JavaScript can help with true action-driven animation. Here's a quick sketch showing how I'd solve this problem in React:
function Button({ children }) { /* We track 4 actions: • "hovering", the user is mousing over the button • "depressed", the user is pressing down on it • "released", the user has released the button • "exited", the user has moused away */ const [mostRecentAction, setMostRecentAction] = React.useState(null); /* Calculate the right styles based on the action */ let styles = {}; if (mostRecentAction === 'hovering') { styles = { transform: 'translateY(-6px)', transition: 'transform 250ms' }; } else if (mostRecentAction === 'depressed') { styles = { transform: 'translateY(-2px)', transition: 'transform 50ms' }; } else if (mostRecentAction === 'released') { styles = { transform: 'translateY(-6px)', transition: 'transform 200ms' }; } else { // The default value // Used initially, before any actions have occurred, and also // when exiting. styles = { transform: 'translateY(-4px)', transition: 'transform 500ms' }; } return ( <button style={styles} // Set the action based on the JS event: onMouseEnter={() => setMostRecentAction('hovering')} onMouseDown={() => setMostRecentAction('depressed')} onMouseUp={() => setMostRecentAction('released')} onMouseLeave={() => setMostRecentAction('exited')}> {children} </button> ); }
Reactive Animation is the art of making animations that respond to user input in real-time (e.g., mouse movement, scrolling, etc).
-
Example: Parallax Effect
-
A parallax effect is when the background moves at a different speed than the foreground, creating a sense of depth.

-
We can create a parallax effect by using the
mousemoveevent to track the user's mouse position, and then using that position to update thetransformproperty of the background element.<style> .container { perspective: 1000px; } .background { width: 100%; height: 100%; background: url('background.jpg'); background-size: cover; transform: translateZ(-1px) scale(2); transition: transform 100ms; } </style> <div class="container"> <div class="background" id="background"></div> </div> <script> const background = document.getElementById('background'); document.addEventListener('mousemove', event => { const x = event.clientX / window.innerWidth - 0.5; const y = event.clientY / window.innerHeight - 0.5; background.style.transform = `translateZ(-1px) scale(2) translate(${x * 20}px, ${ y * 20 }px)`; }); </script>
-
More here: CSS Parallax Effect
-
-
Example: Mouse cursor effect (Animated Cursor)
-
We can create a cursor effect by using the
mousemoveevent to track the user's mouse position, and then using that position to update thetransformproperty of the cursor element.
<style> .cursor { width: 20px; height: 20px; border: 2px solid black; border-radius: 50%; position: absolute; pointer-events: none; transition: transform 100ms; } </style> <!-- The cursor circle element that will follow the mouse --> <div class="cursor" id="cursor"></div> <script> const cursor = document.getElementById('cursor'); document.addEventListener('mousemove', event => { const x = event.clientX; const y = event.clientY; cursor.style.transform = `translate(${x}px, ${y}px)`; }); </script>
-
More here: Animated Cursor
-
Orchestration is the art of coordinating multiple animations to create a cohesive, delightful experience.
In Action-Driven Animation section, we saw how we can improve a modal animation by differentiating between enter and exit actions. We can improve that animation even more by sequencing it.
-
Example: Modal Animation
-
every modal contains multiple different elements, We can vastly improve the modal's animation by orchestrating the different elements, as Instead of everything happening all at once, the individual elements are staggered:

- The backdrop starts fading in right away, and lasts a long time (
1000ms). - The modal waits for
250ms, and then slides in over400ms - The close button is now hidden by default, and is given its own unique transition. It starts animating after
600ms, and lasts250ms.
- The backdrop starts fading in right away, and lasts a long time (
-
It may seem like overkill, but this level of attention-to-detail is key for creating next-level animations. This is the secret sauce.
-
Here's a high-level sketch of our sequenced modal animation, using React:
function Modal({ isOpen, handleDismiss, children }) { return ( <Wrapper> <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} /> <DialogContent style={{ transform: isOpen ? 'translateY(0vh)' : 'translateY(100vh)', transition: 'transform', transitionDuration: isOpen ? '400ms' : '250ms', transitionDelay: isOpen ? '250ms' : '0ms', transitionTimingFunction: isOpen ? 'ease-out' : 'ease-in' }}> <ButtonWrapper> <CloseButton onClick={handleDismiss} style={{ opacity: isOpen ? 1 : 0, transform: isOpen ? 'translateY(0)' : 'translateY(25%)', transition: 'opacity, transform', transitionDuration: '250ms', transitionDelay: isOpen ? '600ms' : '0ms' }} /> </ButtonWrapper> {children} </DialogContent> </Wrapper> ); }
-
-
The
transitionEndevent-
Another way to manage orchestration is to use the
transitionEndevent.element.addEventListener('onTransitionEnd', () => { // Whenever a transition completes on the target element, // this function will be called. });
-
Instead of fiddling with delays, we instruct one animation to start the moment another one ends. This can clean up our code, and make it easier to reason about the relationship between elements.
-
EX:
element.addEventListener('transitionend', () => { // When the backdrop finishes fading in, we'll start sliding in the modal modal.style.transform = 'translateY(0)'; });
-
Motion can be disorienting for some users, and can even trigger motion sickness. It's important to provide a way for users to opt-out of animations.
- Modern operating systems offer a remedy for this: users can opt out of animations. The setting is meant primarily for the OS, but websites and web applications can now access that value and use it in our CSS and JS. It's our job to check and respect that value.
Vestibular disorders are a group of conditions that can cause dizziness, vertigo, and nausea due to the way the brain processes information from the inner ear. Motion on a screen can trigger these symptoms, so it's important to provide a way for users to opt-out of animations.
Usually these symptoms are triggered by animations that have a lot of motion, or that move quickly. and usually not triggered by subtle animations like fades or slow transitions.
-
Opting out of animations
-
For a few years now, operating systems have been letting users request a motion-free experience, typically within the Accessibility settings:

-
Happily, this setting now exists in all mainstream operating systems, including desktop (MacOS 10.12+, Windows 7+, Linux) and mobile (iOS, Android 9+). You can google "reduce animations [operating system]" to find the specific instructions for your device.
-
Apple added a media query that Safari could use to hook into this setting:
prefers-reduced-motion. In the years since, other browsers and operating systems have followed suit. Today, browser support is very good.
-
-
In development, we can use the devtools to simulate this setting:

- Open devtools
- Open the Command Menu (Cmd + Shift + P)
- Type "reduced motion"
- Select "Emulate prefers-reduced-motion: reduce"
.fancy-box {
width: 100px;
height: 100px;
transform: scale(1);
transition: transform 300ms;
}
.fancy-box:hover {
transform: scale(1.2);
}
@media (prefers-reduced-motion: reduce) {
.fancy-box {
transition: none;
}
}-
This media query is a game-changer. It lets us write CSS that respects the user's preference, and it's supported in all modern browsers.
-
If they've ticked the "reduce animations" checkbox,
prefers-reduced-motionwill be set toreduce. The CSS rules in that media query will apply, disabling the transition on our .fancy-box selector. -
This might not work for older browsers, and there's another approach for this, which is to only apply animation when the user has no preference:
/* ...other styles */ @media (prefers-reduced-motion: no-preference) { .fancy-box { transition: transform 300ms; } }
- This way, we're only applying the transition when the user has no preference. If they've opted out of animations, the transition will be disabled.
-
The media query shown above works great for animations that take place entirely from within CSS (eg. transitions, keyframe animations). However, there are many types of animations that cannot be done entirely through CSS like:
- Animations using spring physics.
- Animations involving the cursor coordinates, scroll position, or other “environment” factors.
- HTML5 Canvas animations.
- Certain kinds of SVG animations.
-
Fortunately, we can access the value of the media query from within JS. Here's a snippet:
function getPrefersReducedMotion() { const mediaQueryList = window.matchMedia('(prefers-reduced-motion: no-preference)'); const prefersReducedMotion = !mediaQueryList.matches; return prefersReducedMotion; } // ------------- or ------------- if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { // Turn off animations }
- This function will return true if the user prefers reduced motion (has ticked the "reduce motion" checkbox), or they're using an older browser and we don't know what their true preference is. If it returns false, it means the user has no preference, and we should enable our animations.
-
We can also use event listeners to update this value when it changes:
const mediaQueryList = window.matchMedia('(prefers-reduced-motion: no-preference)'); const listener = event => { const getPrefersReducedMotion = getPrefersReducedMotion(); }; mediaQueryList.addListener(listener); // Later: mediaQueryList.removeListener(listener);
- This listener will fire when the user toggles the "Reduce motion" checkbox in their operating system.
If you want to use this value in your React applications, you can create a custom hook based on this JS logic.
There are some caveats, especially around server-side rendering. There's a blog post on this topic, “Accessible Animations in React”. Check it out if you need to support SSR.
-
In React, we can use the
useMediahook to access the value of the media query:import { useMedia } from 'react-use'; function MyComponent() { const prefersReducedMotion = useMedia('(prefers-reduced-motion: reduce)'); return <div>{prefersReducedMotion ? 'Reduced motion' : 'Normal motion'}</div>; }
- This hook will return
trueif the user prefers reduced motion, andfalseotherwise.
- This hook will return
.spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
animation: spin 2s linear infinite;
}-
To do it with pure CSS, use
input[type=checkbox]to toggle the state<input type="checkbox" id="menu" /> <label for="menu"> <div>1</div> <div>2</div> <div>3</div> </label>
input[type='checkbox'] { display: none; } label { display: block; width: 50px; height: 50px; background: #333; position: relative; cursor: pointer; } label div { width: 30px; height: 5px; background: #fff; position: absolute; left: 10px; transition: 0.5s; } input[type='checkbox']:checked + label div:nth-child(1) { transform: rotate(45deg); top: 22px; }
Read this first for reference: Rotating elements (3D perspective)
-
Markup:
<!-- Acceptance criteria: • The # of columns should be variable (at least 200px each) • The max width for the grid should be 650px • We're only worrying about desktop viewports • The back of the card should be black with white text. • You can't remove any semantic (non-div) HTML elements. You can add some if it's helpful, though it's possible to solve it without changing the markup. --> <ul class="wrapper"> <li class="item"> <a href="/"> <figure> <div class="front"> <img alt="A white building, disappearing into fog. Architecture" src="./architecture-nick-wessaert.jpg" /> </div> <figcaption class="back"> Photo by <span class="photographer">Nick Wessaert</span> </figcaption> </figure> </a> </li> <li class="item"> <a href="/"> <figure> <div class="front"> <img alt="A complex mix of support pillars. Architecture" src="./architecture-alvaro-pinot.jpg" /> </div> <figcaption class="back"> Photo by <span class="photographer">Alvaro Pinot</span> </figcaption> </figure> </a> </li> <li class="item"> <a href="/"> <figure> <div class="front"> <img alt="A white building with staggered balconies. Architecture" src="./architecture-grant-lemons.jpg" /> </div> <figcaption class="back"> Photo by <span class="photographer">Grant Lemons</span> </figcaption> </figure> </a> </li> <li class="item"> <a href="/"> <figure> <div class="front"> <img alt="A unique building with inset curves. Architecture" src="./architecture-julien-moreau.jpg" /> </div> <figcaption class="back"> Photo by <span class="photographer">Julien Moreau</span> </figcaption> </figure> </a> </li> <li class="item"> <a href="/"> <figure> <div class="front"> <img alt="A white building with wavy balconies. Architecture" src="./architecture-christian-perner.jpg" /> </div> <figcaption class="back"> Photo by <span class="photographer">Christian Perner</span> </figcaption> </figure> </a> </li> <li class="item"> <a href="/"> <figure> <div class="front"> <img alt="A modular building against a blue sky. Architecture" src="./architecture-joel-filipe.jpg" /> </div> <figcaption class="back"> Photo by <span class="photographer">Joel Filipe</span> </figcaption> </figure> </a> </li> <li class="item"> <a href="/"> <figure> <div class="front"> <img alt="A wide-open outdoor concrete area. Architecture" src="./architecture-hugo-sousa.jpg" /> </div> <figcaption class="back"> Photo by <span class="photographer">Hugo Sousa</span> </figcaption> </figure> </a> </li> <li class="item"> <a href="/"> <figure> <div class="front"> <img alt="A concrete building with white protrusions. Architecture" src="./architecture-joel-filipe-2.jpg" /> </div> <figcaption class="back"> Photo by <span class="photographer">Joel Filipe</span> </figcaption> </figure> </a> </li> <li class="item"> <a href="/"> <figure> <div class="front"> <img alt="An igloo-style building. Architecture" src="./architecture-mitchell-luo.jpg" /> </div> <figcaption class="back"> Photo by <span class="photographer">Mitchell Luo</span> </figcaption> </figure> </a> </li> </ul>
-
Styling
.wrapper { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); max-width: 650px; gap: 16px; padding: 16px 0; margin: 0 auto; perspective: 1000px; } .item img { display: block; width: 100%; height: 200px; object-fit: cover; } .front, .back { will-change: transform; backface-visibility: hidden; /* Vendor prefix for Safari */ -webkit-backface-visibility: hidden; } .back { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: white; color: black; transform: rotateY(-180deg); } .item figure { position: relative; } .item a:hover .front, .item a:focus .front { transform: rotateY(180deg); } .item a:hover .back, .item a:focus .back { transform: rotateY(0deg); } @supports (aspect-ratio: 1 / 1) { .item img { height: revert; aspect-ratio: 1 / 1; } } @media (prefers-reduced-motion: no-preference) { .item a:hover .front, .item a:focus .front { transition: transform 400ms; } .item a:hover .back, .item a:focus .back { transition: transform 400ms; } .front, .back { transition: transform 800ms 150ms; } .back { background-color: black; color: white; } }
<div class="wrapper">
<a href="/" class="card-link">
<article class="card">
<img src="/images/logos/chrome.svg" />
</article>
</a>
<a href="/" class="card-link">
<article class="card">
<img src="/images/logos/firefox.svg" />
</article>
</a>
<a href="/" class="card-link">
<article class="card">
<img src="/images/logos/safari.png" />
</article>
</a>
<a href="/" class="card-link">
<article class="card">
<img src="/images/logos/edge.svg" />
</article>
</a>
<a href="/" class="card-link">
<article class="card">
<img src="/images/logos/opera.svg" />
</article>
</a>
</div>.wrapper {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 8px;
perspective: 500px;
transform-style: preserve-3d; /* 👈 Very important for 3D transforms */
}
.card {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 200px;
background: white;
border-radius: 8px;
border: 2px solid hsl(240deg 100% 75%);
will-change: transform;
transform: rotateX(0deg);
transition: transform 750ms;
transform-origin: top center;
}
.card img {
width: 64px;
height: 64px;
}
.card-link:hover .card,
.card-link:focus .card {
transform: rotateX(-35deg);
transition: transform 250ms;
}
.card-link:focus {
outline: none;
}
.card-link:focus .card {
outline: 3px solid hsl(240deg 100% 50%);
outline-offset: 2px;
}- The trick here is:
- to have 2 elements: one for the front and one for the back. The front element is the one that's visible by default, and the back element is hidden behind it (because the're both absolutely positioned).
- When the user hovers over the link, we translate the front element up (to leave a room for the back element), and translate the back element up to be visible.
- Trick: Use CSS variables to control the
fromandtovalues translated positions.
<nav>
<ul>
<li>
<a href="/">
<span class="text main-text">Seraglio</span>
<span class="text hover-text">Seraglio</span>
</a>
</li>
<li>
<a href="/">
<span class="text main-text">Sumptuous</span>
<span class="text hover-text">Sumptuous</span>
</a>
</li>
<li>
<a href="/">
<span class="text main-text">Scintilla</span>
<span class="text hover-text">Scintilla</span>
</a>
</li>
<li>
<a href="/">
<span class="text main-text">Palimpsest</span>
<span class="text hover-text">Palimpsest</span>
</a>
</li>
<li>
<a href="/">
<span class="text main-text">Assemblage</span>
<span class="text hover-text">Assemblage</span>
</a>
</li>
</ul>
</nav>a {
position: relative;
display: block;
font-size: 1.125rem;
text-transform: uppercase;
text-decoration: none;
color: var(--color-gray-900);
font-weight: --WEIGHTS-medium;
/*
Text slide-up effect
*/
overflow: hidden;
&:first-of-type {
color: var(--color-secondary);
}
}
.text {
display: block;
transform: translateY(var(--translate-from));
transition: transform 500ms;
/* Only enable animations when the user has no preference for better UX */
@media (prefers-reduced-motion: no-preference) {
nav:hover & {
transition: transform 250ms;
transform: translateY(var(--translate-to));
}
}
}
.main-text {
--translate-from: 0% /* 👈 */
--translate-to: -100%; /* 👈 */
}
.hover-text {
--translate-from: 100%; /* 👈 */
--translate-to: 0%; /* 👈 */
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
font-weight: --WEIGHTS-bold;
}Instead of re-inventing the wheel, you can use libraries to animate elements:
- Animate.css
- Hover.css
- Magic Animations
- Bounce.js
- Anime.js
- GreenSock Animation Platform (GSAP) -> it's the most powerful and flexible animation library
- Here's an entire notes file for it: FE-Animation
- React Spring
-
Don't use hover or any effect without using
transitionproperty -
Question: what will happen if you use
transitionproperty inside the:hoverbody?button { background-color: red; } button:hover { transition: background-color 1s; background-color: blue; }
-
it will make the transition happen when the mouse enters the element and not when it leaves it
-
instead of writing different final state in
100%, you can call it any name and useto <name> -
to see animation for an element in DevTools -> ctrl + shift + p and type animation
-
Interview question: "What if you add the
transitionin the:hoverbody instead of the normal body?"- it will work but it's not recommended as it will make the transition happen when the mouse leaves the element and not when it enters it
-
transformis more efficient thanpositionfor animation -
to change anchor point of element =>
transform-origin, find more here -
to disable animation (some users prefer no animation), we can use this media-query:
/* If the user has expressed their preference for reduced motion, then don't use animations on buttons. */ @media (prefers-reduced-motion: reduce) { button { animation: none; } /* or */ * { animation-duration: 0s !important; transition-duration: 0s !important; } }
-
We can inspect the animation in Chrome DevTools ->
command + shift + p->show animations

- More here in the chrome docs











