People increasingly select their tools based on the overall experience rather than just functionality. A predictable and delightful experience is what makes a product stand out from a crowded market. That's why companies invest in design engineers for example. Animations can play a big role in creating such experiences.
Great animations are hard, as there are many aspects to consider. From easing and timing to accessibility and performance. This post is a collection of principles that, in my opinion, make animations great.
People love the dynamic island, because it feels natural, almost like a living organism.
This natural motion makes things easier to understand and is a major reason why mobile apps feel better than web apps. Changes in web apps often occur instantly, which makes the experience feel artificial and unfamiliar, since nothing in world around us disappears or appears instantly.
I highly suggest playing around with spring animations in your projects. Below is a visualizer that can help you understand how spring animations are influenced by different parameters.
This visualizer is inspired by Morses's work.
Fast animations improve the perceived performance. Take a look at these two spinners for example, which one would load faster?
They would both take the same time to load, but the one on the right gives you an impression as if it's working very hard to load the data for you.
Snappy animations feel responsive and connected to user's actions.
Family's iOS app is a perfect example of a snappy interface.
The best type of easing for this purpose is ease-out
. It starts fast and slows down at the end, which gives the impression of a quick response, while maintaining a smooth transition. Your animations should also usually be shorter than 300ms.
It's easy to start adding animations everywhere. The user then becomes overwhelmed and animations lose their impact. We need to pace them through the experience and add them in places where they enrich the information on the page.
A good example is this animation I made for Vercel. It explains how v0 works. While the animation is arguably entertaining to watch, it's also insightful.
We can also use animations to indicate a change in state, like with the App Store cards. An enter or exit animation for a modal is also a good example.
Before you add an animation, you should also consider how often the user will see it. A good tip here is to never animate keyboard initiated actions. These actions are repeated sometimes hundreds of times a day, an animation would make them feel slow and disconnected from user's actions.
I use Raycast frequently and can't imagine how frustrating it would be if every time I opened it, I was greeted with a 500ms enter animation.
Raycast has no animations and it feels right.
If our animations won't run at 60 frames per second, everything else we've talked about becomes useless.
The rule of thumb here is that you should try to animate with transform
and opacity
as they only trigger the third rendering step (composite), while padding or margin triggers all three (layout, paint, composite). The less work the browser has to do, the better the performance.
If the main thread is busy, you should animate using hardware-accelerated animations like CSS or WAAPI (Web Animation API). A hardware-accelerated animation will remain smooth, no matter how busy the main thread is. Keep in mind that even if you do animate with CSS, not all properties are hardware-accelerated, but if you stick to transform
and opacity
, you should be fine.
The more logos you add in the demo below, the laggier the Framer Motion animation will become, as it uses requestAnimationFrame
under the hood, which is not hardware-accelerated.
This example is inspired by the Sidebar Animation Performance post by Josh Wootonn.
This issue happened in Vercel's dashboard where we animated the active tab. The transition was done with Shared Layout Animations, and because the browser was busy loading the new page, the animation dropped frames. We fixed this by using CSS animations which moved the animation off the CPU.
Interruptibility helps your animations feel more natural and responsive. It allows the user to change the state of the animation at any time while maintaining a smooth transition. Try clicking on one of the items below and quickly closing it by pressing the escape key.
Explore unknown galaxies.
They are coming for you.
Scarry ghosts.
Find the treasure.
Be careful.
The example above is built with Framer Motion, which supports interruptible animations. If you prefer to stick with CSS, you can replace your animations with transitions. A CSS transition can be interrupted and smoothly transition to a new value, even before the first transition has finished. You can see the difference below.
Animations are used to strategically improve an experience. To some people, animations actually degrade the experience. Animations can make people feel sick or get distracted. That’s not the experience we want to build. To prevent degrading the experience, our animations need to account for people who don’t want animations.
Usually in this step we would explain why this thing exists and what it does. Also, we would show a button to go to the next step.
This component animates opacity only when the user prefers reduced motion.
We can use a media query in CSS to adjust the animation based on the user's preference.
.element {
animation: bounce 0.2s;
}
@media (prefers-reduced-motion: reduce) {
.element {
animation: fade 0.2s;
}
}
.element {
animation: bounce 0.2s;
}
@media (prefers-reduced-motion: reduce) {
.element {
animation: fade 0.2s;
}
}
Or use a hook if we are using Framer Motion for example.
import { useReducedMotion, motion } from "framer-motion"
export function Sidebar({ isOpen }) {
const shouldReduceMotion = useReducedMotion();
const closedX = shouldReduceMotion ? 0 : "-100%";
return (
<motion.div animate={{
opacity: isOpen ? 1 : 0,
x: isOpen ? 0 : closedX
}} />
)
}
import { useReducedMotion, motion } from "framer-motion"
export function Sidebar({ isOpen }) {
const shouldReduceMotion = useReducedMotion();
const closedX = shouldReduceMotion ? 0 : "-100%";
return (
<motion.div animate={{
opacity: isOpen ? 1 : 0,
x: isOpen ? 0 : closedX
}} />
)
}
People love using Sonner mainly, because the animation feels satisfying. But I also think it's because the whole experience of using it is cohesive.
The easing and duration of the animation fits the vibe of the whole library. It's a bit slower than usual and uses ease
as the easing type rather than ease-out
to make it feel more elegant. It's in line with the toast design, page design, its name etc.
Another example of this is Family's drawer that I recreated on the web. I don't know the exact animation values that Family used, so it's not as good, but people still seem to love it.
I think it's mainly because the easings used in the animation feel right. The opacity change in exiting and entering items works well with the height animation. In this case, it was a matter of trial and error until it felt right. But that's often the case with animations—you have to be patient.
Take some time to review your animations. I like to review my work the next day because I can see it with fresh eyes and notice imperfections I didn't see before.
This post should give you a good starting point for creating great animations. But if you'd want to learn more, I cover all the principles we talked about in-depth in my course Animations on the Web. We also build all the components above and more there.
Check out "Animations on the Web"The goal of these posts is to create helpful content for engineers and designers, I try to do the same with my newsletter. I'll let you know when I publish new content, and share exclusive, newsletter-only content once a month.
No spam, unsubscribe at any time.